Chapter 7. Operationalizing Machine Learning Models

All of the machine learning models presented so far in this book are written in Python. Models don’t have to be written in Python, but many are, thanks in part to the numerous world-class Python libraries available, including Pandas and Scikit-Learn. ML models written in Python are easily consumed in Python apps. Calling them from other languages such as C++, Java, and C# requires a little more work. You can’t simply call a Python function from C++ as if it were a C++ function. So how do you invoke models written in Python from apps written in other languages? Put another way, how do you operationalize Python models such that they are usable in any app on any platform written in any programming language?

The diagram on the left in Figure 7-1 shows one strategy: wrap the model in a web service and expose its functionality through a REST API. Then any client that can generate an HTTP(S) request can invoke the model. It’s relatively easy to do the wrapping with help from Python frameworks such as Flask. The web service can be hosted locally or in the cloud, and it can be containerized for easy deployment using tools such as Docker.

Figure 7-1. Architectures for consuming Python models in other languages

The diagram on the right shows another strategy—one that’s relatively new but rapidly growing in popularity. It involves exporting a Python model to a platform-agnostic format called ONNX, short for Open Neural Network Exchange, and then using an ONNX runtime to load the model in Java, C++, C#, and other programming languages. Once loaded, the model can be called via the ONNX runtime.

Of course, if the client app and the model are written in the same language, you need neither a web service nor ONNX. In this chapter, I’ll walk you through several scenarios:

  • How to save a trained Python model and invoke it from a Python client

  • How to invoke a Python model from a non-Python client using a web service

  • How to containerize a Python model (and web service) for easy deployment

  • How to use ONNX to invoke a Python model from other programming languages

  • How to write machine learning models in C# rather than Python

I’ll finish up by demonstrating a novel way to operationalize a machine learning model by exposing its functionality through Microsoft Excel. There’s a lot to cover, so let’s get started.

Consuming a Python Model from a Python Client

Ostensibly, invoking a Python model from a Python client is simple: just call predict (or, for a classifier, predict_proba) on the model. Of course, you don’t want to have to retrain the model every time you use it. You want to train it once, and then empower a client app to re-create the model in its trained state. For that, Python programmers use the Python pickle module.

To demonstrate, the following code builds and trains the Titanic model featured in Chapter 3. Rather than use the model to make predictions, however, it saves the model to a .pkl file (it “pickles” the model) with a call to pickle.dump on the final line:

import pickle
import pandas as pd
from sklearn.linear_model import LogisticRegression

df = pd.read_csv('Data/titanic.csv')
df = df[['Survived', 'Age', 'Sex', 'Pclass']]
df = pd.get_dummies(df, columns=['Sex', 'Pclass'])
df.dropna(inplace=True)

x = df.drop('Survived', axis=1)
y = df['Survived']

model = LogisticRegression(random_state=0)
model.fit(x, y)

pickle.dump(model, open('titanic.pkl', 'wb'))

To invoke the model, a Python client uses pickle.load to deserialize the model from the .pkl file, re-creating the model in its trained state, and calls predict_proba to compute the odds of a passenger’s survival:

import pickle
import pandas as pd

model = pickle.load(open('titanic.pkl', 'rb'))

female = pd.DataFrame({ 'Age': [30], 'Sex_female': [1], 'Sex_male': [0],
                        'Pclass_1': [1], 'Pclass_2': [0], 'Pclass_3': [0] })

probability = model.predict_proba(female)[0][1]
print(f'Probability of survival: {probability:.1%}')

Now the client can use the model to make a prediction without retraining it. And once the model is loaded, it can persist for the lifetime of the client and be called upon for predictions whenever needed.

Chapter 5 introduced Scikit’s make_pipeline function, which allows estimators (objects that make predictions) and transformers (objects that transform data input to a model) to be combined into a single unit, or pipeline. The pickle module can be used to serialize and deserialize pipelines too. Example 7-1 recasts the sentiment analysis model featured in Chapter 4 to use make_pipeline to combine a CountVec⁠tor⁠izer for vectorizing text with a LogisticRegression object for classifying text. The call to pickle.dump saves the model, CountVectorizer and all.

Example 7-1. Training and saving a sentiment analysis pipeline
import pickle
import pandas as pd
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import make_pipeline

df = pd.read_csv('Data/reviews.csv', encoding="ISO-8859-1")
df = df.drop_duplicates()

x = df['Text']
y = df['Sentiment']

vectorizer = CountVectorizer(ngram_range=(1, 2), stop_words='english',
                             min_df=20)

model = LogisticRegression(max_iter=1000, random_state=0)
pipe = make_pipeline(vectorizer, model)
pipe.fit(x, y)

pickle.dump(pipe, open('sentiment.pkl', 'wb'))

A Python client can deserialize the pipeline and call predict_proba to score a line of text for sentiment with a few simple lines of code:

import pickle

pipe = pickle.load(open('sentiment.pkl', 'rb'))
score = pipe.predict_proba(['Great food and excellent service!'])[0][1]
print(score)

Pickling in this manner works not just with CountVectorizer but with other transformers as well, such as StandardScaler.

Note

Pickling a pipeline containing a CountVectorizer can produce a large .pkl file. In this example, sentiment.pkl is 50 MB in length because it contains the entire vocabulary built by CountVectorizer from the training text. Remove the min_df=20 parameter and the file swells to nearly 90 MB.

The solution is to replace CountVectorizer with Hashing​Vec⁠tor⁠izer, which doesn’t create a vocabulary but instead uses word hashes to index a table of word frequencies. That reduces sentiment.pkl to 8 MB—without the min_df parameter, which HashingVectorizer doesn’t support anyway.

If you’d like to write a standalone Python client that performs sentiment analysis, run the code in Example 7-1 in a Jupyter notebook to generate sentiment.pkl. Optionally, you can change CountVectorizer to HashingVectorizer and remove the min_df parameter to reduce the size of the .pkl file. Then create a Python script named sentiment.py containing the following code:

import pickle, sys

# Get the text to analyze
if len(sys.argv) > 1:
    text = sys.argv[1]
else:
    text = input('Text to analyze: ')

# Load the pipeline containing the model and the vectorizer
pipe = pickle.load(open('sentiment.pkl', 'rb'))

# Pass the input text to the pipeline and print the result
score = pipe.predict_proba([text])[0][1]
print(score)

Copy sentiment.pkl into the same directory as sentiment.py, and then pop out to the command line and run the script:

python sentiment.py "Great food and excellent service!"

The output should look something like this, which is proof that you succeeded in re-creating the model in its trained state and invoking it to analyze the input text for sentiment:

Note that the sentiment score will differ slightly if you replaced CountVectorizer with HashingVectorizer, in part due to the omission of the min_df parameter.

Versioning Pickle Files

Generally speaking, a model pickled (saved) with one version of Scikit can’t be unpickled with another version. Sometimes the result is warning messages; other times, it doesn’t work at all. Be sure to save models with the same version of Scikit that you use to consume them. This requires a bit of planning from an engineering perspective, because if you store serialized models in a centralized repository and update the version of Scikit used in your apps, you’ll need to update the saved models too.

Which prompts some interesting questions: How do you set up a repository for machine learning models? How do you deploy models from the repository to the devices that host them? For that matter, how do you version those models as well as the datasets you train them with?

The answers come from a nascent field known as MLOps, which is short for ML operations. I don’t cover MLOps in this book because it’s a rich subject that is a book unto itself. If you want to learn more, I recommend reading Practical MLOps: Operationalizing Machine Learning Models by Noah Gift and Alfredo Deza (O’Reilly).

Consuming a Python Model from a C# Client

Suppose you wanted to invoke the sentiment analysis model in the previous section from an app written in another language—say, C#. You can’t directly call a Python function from C#, but you can wrap a Python model in a web service and expose its predict (or predict_proba) method using a REST API. One way to code the web service is to use Flask, a popular framework for building websites and web services in Python.

To see for yourself, make sure Flask is installed on your computer. Then create a file named app.py and paste in the following code. This code uses Flask to implement a Python web service that listens on port 5000:

import pickle
from flask import Flask, request

app = Flask(__name__)
pipe = pickle.load(open('sentiment.pkl', 'rb'))

@app.route('/analyze', methods=['GET'])
def analyze():
    if 'text' in request.args:
        text = request.args.get('text')
    else:
        return 'No string to analyze'

    score = pipe.predict_proba([text])[0][1]
    return str(score)

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

At startup, the service deserializes the pipeline comprising the sentiment analysis model in sentiment.pkl. The @app.route statement decorating the analyze function tells Flask to call the function when the service’s analyze method is called. If the service is hosted locally, the following request invokes analyze and returns a string containing a sentiment score for the text in the query string:

http://localhost:5000/analyze?text=Great food and excellent service!

To demonstrate, go to the directory where app.py is located (make sure sentiment.pkl is there too) and start Flask by typing:

flask run

Then go to a separate command prompt and use a curl command to fire off a request to the URL:

curl -G -w "
" http://localhost:5000/analyze --data-urlencode "text=Great food 
and excellent service!"

Here’s the output:

If you have Visual Studio or Visual Studio Code installed on your computer and are set up to compile and run C# apps, you can use the following code in a C# console app to invoke the web service and score a text string for sentiment. Of course, you’re not limited to invoking the web service (and by extension, the model) from C#. Any language will do, because virtually all modern programming languages provide a means for sending HTTP requests.

using System;
using System.Net.Http;
using System.Threading.Tasks;

class Program
{
    static async Task Main(string[] args)
    {
        string text;

        // Get the text to analyze
        if (args.Length > 0)
        {
            text = args[0];
        }
        else
        {
            Console.Write("Text to analyze: ");
            text = Console.ReadLine();
        }

        // Pass the text to the web service
        var client = new HttpClient();
        var url = $"http://localhost:5000/analyze?text={text}";
        var response = await client.GetAsync(url);
        var score = await response.Content.ReadAsStringAsync();

        // Show the sentiment score
        Console.WriteLine(score);
    }
}

This web service is a simple one that reads input from a query string and returns a string. For more complex input and output, you can serialize the input into JSON and transmit it in the body of an HTTP POST, and you can return a JSON payload in the response. For a tutorial demonstrating how to do it in Python, see the article “Python Post JSON Using Requests Library”.

Containerizing a Machine Learning Model

One downside to wrapping a machine learning model in a web service and running it locally is that the client computer must have Python installed, as well as all the packages that the model and web service require. An alternative is to host the web service in the cloud where it can be called via the internet. It’s not hard to go out to Azure or AWS, spin up a virtual machine (VM), and install the software there. But there’s a better way. That better way is containers.

Containers have revolutionized the way software is built and deployed. A container includes an app and everything the app needs to run, including a runtime (for example, Python), the packages the app relies on, and even a virtual filesystem. If you’re not familiar with containers, think of them as lightweight VMs that start quickly and consume far less memory. Docker is the world’s most popular container platform, although it is rapidly being supplanted by Kubernetes.

Containers are created from container images, which serve as blueprints for containers in the same way that classes in object-oriented programming languages constitute blueprints for objects. The first step in creating a Docker container image containing the sentiment analysis model and web service is to create a file named Dockerfile (no filename extension) in the same directory as app.py and sentiment.pkl and then paste the following statements into it:

FROM python:3.8
RUN pip install flask numpy scipy scikit-learn && 
    mkdir /app
COPY app.py /app
COPY sentiment.pkl /app
WORKDIR /app
EXPOSE 5000
ENTRYPOINT ["python"]
CMD ["app.py"]

A Dockerfile contains instructions for building a container image. This one creates a container image that includes a Python runtime, several Python packages such as Flask and Scikit-Learn, and app.py and sentiment.pkl. It also instructs the Docker runtime that hosts the container to open port 5000 for HTTP requests and to execute app.py when the container starts.

There are several ways to build a container image from a Dockerfile. If Docker is installed on your computer, you can use a docker build command:

docker build -t sentiment-server .

Alternatively, you can upload the Dockerfile to a cloud platform such as Microsoft Azure and build it there. This prevents you from having to have Docker installed on the local machine, and it makes it easy to store the resulting container image in the cloud. Container images are stored in container registries, and modern cloud platforms host container registries as well as containers. If you launch a container instance in Azure, the web service in the container can be invoked with a URL similar to this one:

http://wintellect.northcentralus.azurecontainer.io:5000/analyze?text=Great food and excellent service!

One of the benefits of hosting the container instance in the cloud is that it can be reached from any client app running on any machine and any operating system, and the computer that hosts the app needs nothing special installed. Containers can be beneficial even if you host the web service locally rather than in the cloud. As long as you deploy a container stack such as the Docker runtime to the local machine, you don’t have to install Python and all the packages that the web service requires. You just launch a container instance and direct HTTP requests to it via localhost.

Using ONNX to Bridge the Language Gap

Is it possible to bridge the gap between a C# client and a Python ML model without using a web service as a middleman? In a word, yes! The solution lies in a four-letter acronym: ONNX. As mentioned earlier, ONNX stands for Open Neural Network Exchange, and it was originally devised to allow deep-learning models written with one framework—for example, TensorFlow—to be used with other frameworks such as PyTorch. But today it can be used with Scikit models too. I’ll say more about ONNX in Chapter 12, but for now, let’s use it to call a Python model directly from an app written in C#—no web service required.

The first step in using ONNX to bridge the language gap is to install Skl2onnx in your Python environment. Then use that package’s convert_sklearn method to save a trained Scikit model to a .onnx file. Here’s a short code snippet that saves the sentiment analysis model in Example 7-1 to a file named sentiment.onnx. The initial_types parameter specifies that the model expects one input value: a string containing the text to score for sentiment:

from skl2onnx import convert_sklearn
from skl2onnx.common.data_types import StringTensorType

initial_type = [('string_input', StringTensorType([None, 1]))]
onnx = convert_sklearn(pipe, initial_types=initial_type)

with open('sentiment.onnx', 'wb') as f:
    f.write(onnx.SerializeToString())

If you wish to consume the model from Python, you first install a Python package named Onnxruntime containing the ONNX runtime, also known as the ORT. This provides support for loading and running ONNX models. Then you call the runtime’s InferenceSession method with a path to a .onnx file to deserialize the model, and call run on the returned session object to call the model’s predict or predict_proba method. Here’s how it looks in code:

import numpy as np
import onnxruntime as rt

session = rt.InferenceSession('sentiment.onnx')
input_name = session.get_inputs()[0].name
label_name = session.get_outputs()[1].name # 0 = predict, 1 = predict_proba

input = np.array('Great food and excellent service!').reshape(1, -1)
score = session.run([label_name], { input_name: input })[0][0][1]
print(score)

Note that the string input to the model is passed as a NumPy array. The value returned from the run method reveals the sentiment score returned by predict_proba. If you’d rather call predict to predict a class, change ses⁠sion​.get_outputs()[1].name to session.get​_outputs()[0].name.

Note

In real life, you wouldn’t load the model every time you call it. You’d load it once, allow it to persist for the lifetime of the client, and call it whenever you want to make a prediction.

Of course, the whole point of this discussion is to call the model from C# instead of Python. That’s not difficult either, thanks to a NuGet package from Microsoft called Microsoft.ML.OnnxRuntime. You can install it from the command line or using an integrated development environment such as Visual Studio. Then it’s a relatively simple matter to write a C# console app that re-creates the trained sentiment analysis model from the .onnx file and calls it to score a text string:

using System;
using Microsoft.ML.OnnxRuntime;
using Microsoft.ML.OnnxRuntime.Tensors;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main(string[] args)
    {
        string text;

        // Get the text to analyze
        if (args.Length > 0)
        {
            text = args[0];
        }
        else
        {
            Console.Write("Text to analyze: ");
            text = Console.ReadLine();
        }


        // Create the model and pass the text to it
        var tensor = new DenseTensor<string>(new string[]
            { text }, new int[] { 1, 1 });

        var input = new List<NamedOnnxValue>
        {
            NamedOnnxValue.CreateFromTensor<string>("string_input", tensor)
        };

        var session = new InferenceSession("sentiment.onnx");
        var output = session.Run(input)
            .ToList().Last().AsEnumerable<NamedOnnxValue>();

        var score = output.First().AsDictionary<Int64, float>()[1];
        
        // Show the sentiment score
        Console.WriteLine(score);
    }
}

This code assumes that sentiment.onnx is present in the current directory. Instantiating the InferenceSession object creates the model, and calling Run on the object invokes the model. Under the hood, Run calls the model’s predict and predict_proba methods and makes the results of both available. There’s a little work required to convert C# types into ONNX types and vice versa, but once you get the hang of it, it’s pretty remarkable that you can call a machine learning model written in Python directly from C#.

Here’s another example. Suppose you wanted to consume Chapter 2’s taxi-fare regression model in C#. Recall that the model accepts three floating-point values as input—the day of the week (0–6), the hour of day (0–23), and the distance to travel in miles—and returns a predicted taxi fare. The following Python code saves the trained model in an ONNX file:

from skl2onnx import convert_sklearn
from skl2onnx.common.data_types import FloatTensorType

initial_type = [('float_input', FloatTensorType([None, 3]))]
onnx = convert_sklearn(model, initial_types=initial_type)

with open('taxi.onnx', 'wb') as f:
    f.write(onnx.SerializeToString())

The following C# code loads the model and makes a prediction. The big difference between this and the previous example is that you pass the model an array of three floating-point values rather than a string:

// Package the input
var input = new float[] {
    4.0f,  // Day of week
    17.0f, // Pickup time (hour of day)
    2.0f   // Distance to travel
};

var tensor = new DenseTensor<float>(input, new int[] { 1, 3 });

// Create the model and pass the input to it
var session = new InferenceSession("taxi.onnx");

var output = session.Run(new List<NamedOnnxValue>
{
    NamedOnnxValue.CreateFromTensor("float_input", tensor)
});

var score = output.First().AsTensor<float>().First();

// Show the predicted fare
Console.WriteLine($"{score:#.00}");

So which is faster? Calling a machine learning model wrapped in a web service, or calling the same model using ONNX? I wrote a simple test harness to answer that question. On my computer, calling the sentiment analysis model in a Flask web service running locally required slightly more than 2 seconds per round trip. Calling the same model through the Python ONNX runtime took 0.001 seconds on average. That’s a difference of more than three orders of magnitude. And you would incur additional latency if the web service was hosted on a remote server.

Significantly, ONNX isn’t limited to C# and Python. ONNX runtimes are available for Python, C, C++, C#, Java, JavaScript, and Objective-C, and they run on Windows, Linux, macOS, Android, and iOS. When it comes to projecting machine learning models written in Python to other platforms and languages, ONNX is a game changer. For more information, check out the ONNX runtime website.

Building ML Models in C# with ML.NET

Scikit-Learn is arguably the world’s most popular machine learning framework. The efficacy of the library, the documentation that accompanies it, and the mindshare that surrounds it are the primary reasons more ML models are written in Python than any other language. But Scikit isn’t the only machine learning framework. Others exist for other languages, and if you can code an ML model in the same programming language as the client that consumes it, you can avoid jumping through hoops to operationalize the model.

ML.NET is Microsoft’s free, open source, cross-platform machine learning library for .NET developers. It does most of what Scikit does and a few things that Scikit doesn’t do. And when it comes to writing ML/AI solutions in C#, there is no better tool for the job.

ML.NET derives from an internal library that was developed by Microsoft—and has been used in Microsoft products—for more than a decade. The ML algorithms that it implements have been tried and tested in the real world and tuned to optimize performance and accuracy. Because ML.NET is consumed from C#, you get all the benefits of a compiled programming language, including type safety and fast execution.

A paper published by the ML.NET team at Microsoft in 2019 discusses the motivations and design goals behind ML.NET. It also compares ML.NET’s accuracy and performance to that of Scikit-Learn and another machine learning framework named H2O. Using a 9 GB Amazon review dataset, ML.NET trained a sentiment analysis model to 95% accuracy. Neither Scikit nor H2O could process the dataset due to its size. When all three frameworks were trained on 10% of the dataset, ML.NET achieved the highest accuracy, and trained six times faster than Scikit and almost 10 times faster than H2O.

ML.NET is compatible with Windows, Linux, and macOS. Thanks to an innovation called IDataView, it can handle datasets of virtually unlimited size. While it can’t be used to build neural networks from scratch, it does have the ability to load existing neural networks and use a technique called transfer learning to repurpose those networks to solve domain-specific problems. (Transfer learning will be covered in Chapter 10.) It also builds in ONNX support that allows it to load sophisticated models, including deep-learning models, stored in ONNX format. Finally, it can be consumed in Python and even combined with Scikit-Learn using a set of Python bindings called NimbusML.

If you’re a .NET developer who is interested in machine learning, there has never been a better time to get acquainted with ML.NET. This section isn’t meant to provide an exhaustive treatment of ML.NET but to introduce it, show the basics of building ML models with it, and hopefully whet your appetite enough to motivate you to learn more. There are plenty of great resources available online, including the official ML.NET documentation, a GitHub repo containing ML.NET samples, and the ML.NET cookbook.

Sentiment Analysis with ML.NET

The following C# code uses ML.NET to build and train a sentiment analysis model. It’s equivalent to the Python implementation in Example 7-1:

var context = new MLContext(seed: 0);

// Load the data
var data = context.Data.LoadFromTextFile<Input>("reviews.csv",
    hasHeader: true, separatorChar: ',', allowQuoting: true);

// Split the data into a training set and a test set
var trainTestData = context.Data.TrainTestSplit(data,
    testFraction: 0.2, seed: 0);
var trainData = trainTestData.TrainSet;
var testData = trainTestData.TestSet;

// Build and train the model
var pipeline = context.Transforms.Text.FeaturizeText
    (outputColumnName: "Features", inputColumnName: "Text")
    .Append(context.BinaryClassification.Trainers.SdcaLogisticRegression());

var model = pipeline.Fit(trainData);

// Evaluate the model
var predictions = model.Transform(testData);
var metrics = context.BinaryClassification.Evaluate(predictions);
Console.WriteLine($"AUC: {metrics.AreaUnderPrecisionRecallCurve:P2}");

// Score a line of text for sentiment
var predictor = context.Model.CreatePredictionEngine<Input, Output>(model);
var input = new Input { Text = "Among the best movies I have ever seen"};
var prediction = predictor.Predict(input);

// Show the score
Console.WriteLine($"Sentiment score: {prediction.Probability}");

Every ML.NET app begins by creating an instance of the MLContext class. The optional seed parameter initializes the random-number generator used by ML.NET so that you get repeatable results from one run to the next. MLContext exposes a number of properties through which large parts of the ML.NET API are accessed. One example of this is the call to LoadFromTextFile, which is a DataOperationsCatalog method accessed through MLContext’s Data property.

LoadFromTextFile is one of several methods ML.NET provides for loading data from text files, databases, and other data sources. It returns a data view, which is an object that implements the IDataView interface. Data views in ML.NET are similar to DataFrames in Pandas, with one important difference: whereas DataFrames have to fit in memory, data views do not. Internally, data views use a SQL-like cursor to access data. This means they can wrap a theoretically unlimited amount of data. That’s why ML.NET was able to process the entire 9 GB Amazon dataset, while Scikit and H2O were not.

After loading the data and splitting it for training and testing, the preceding code creates a pipeline containing a TextFeaturizingEstimator object—created with the FeaturizeText method—and an SdcaLogisticRegressionBinaryTrainer object—created with the Sdca​Lo⁠gisticRegression method. This is analogous in Scikit to creating a pipeline containing a CountVectorizer object for vectorizing input text and a LogisticRegression object for fitting a model to the data. Calling Fit on the pipeline trains the model, just like calling fit in Scikit. It’s no coincidence that ML.NET employs some of the same patterns as Scikit. This was done intentionally to impart a sense of familiarity to programmers who use Scikit.

After evaluating the model’s accuracy by computing the area under the precision-recall curve, a call to ModelOperationsCatalog.CreatePredictionEngine creates a prediction engine whose Predict method makes predictions. Unlike Scikit, which has you call predict on the estimator itself, ML.NET encapsulates prediction capability in a separate object, in part so that multiple prediction engines can be created to achieve scalability in high-traffic scenarios.

In this example, Predict accepts an Input object as input and returns an Output object. One of the benefits of building models with ML.NET is strong typing. LoadFromTextFile is a generic method that accepts a class name as a type parameter—in this case, Input. Similarly, CreatePredictionEngine uses type parameters to specify schemas for input and output. The Input and Output classes are application specific and in this instance are defined as follows:

public class Input
{
    [LoadColumn(0)]
    public string Text;

    [LoadColumn(1), ColumnName("Label")]
    public bool Sentiment;
}

public class Output
{
    [ColumnName("PredictedLabel")]
    public bool Prediction { get; set; }

    public float Probability { get; set; }
}

The LoadColumn attributes map columns in the data file to properties in the Input class. Here, they tell ML.NET that values for the Text field come from column 0 in the input file, and values for Sentiment (the 1s and 0s indicating whether the sentiment expressed in the text is positive or negative) come from column 1. The Colum⁠n​Name("Label") attribute identifies the second column as the label column—the one containing the values that the model will attempt to predict.

The Output class defines the output schema. In this example, it contains properties named Prediction and Probability, which, following a prediction, hold the predicted label (0 or 1) and the probability that the sample belongs to the positive class, which doubles as a sentiment score. The ColumnName("PredictedLabel") attribute maps the value returned by Predict to the Output object’s Prediction property.

Note

There is nothing magic about the class names Input and Output. You could name them SentimentData and SentimentPrediction and the code would work just the same.

Saving and Loading ML.NET Models

Earlier, you learned how to use Python’s pickle module to save and load trained models. You do the same in ML.NET by calling ModelOperationsCatalog.Save and ModelOperationsCatalog.Load through the MLContext object’s Model property:

// Save a trained model to a zip file
context.Model.Save(model, data.Schema, "model.zip");

// Load a trained model from a zip file
var model = context.Model.Load("model.zip", out DataViewSchema schema);

This enables clients to re-create a model in its trained state and use it to make predictions without having to train the model repeatedly.

Adding Machine Learning Capabilities to Excel

Want to see a novel way to operationalize a machine learning model? Imagine that you’re a software developer at an internet vacation rentals firm. The company’s communications department has asked you to create a spreadsheet that lets them analyze text for sentiment. The idea is that if sentiment toward the company turns negative on social media, the communications team can get out in front of it.

You already know how to build, train, and save a sentiment analysis model in Python using Scikit. Excel supports user-defined functions (UDFs), which enable users to write custom functions that are called just like SUM(), AVG(), and other functions built into Excel. But UDFs are written in Visual Basic for Applications (VBA), not Python. To marry Scikit with Excel, you need to write UDFs in Python.

Fortunately, there are libraries that let you do just that. One of them is Xlwings, an open source library that combines the power of Excel with the versatility of Python. With it, you can write Python code that loads or creates Excel spreadsheets and manipulates their content, write Python macros triggered by button clicks in Excel, access Excel spreadsheets from Jupyter notebooks, and more. You can also use Xlwings to write Python UDFs for Excel for Windows.

The first step in building the spreadsheet the communications team wants is to configure Excel to trust VBA add-ins. Launch Microsoft Excel and use the File → Options command to open the Excel Options dialog. Click Trust Center in the menu on the left, and then click the Trust Center Settings button. Click Macro Settings on the left, and check the “Trust access to the VBA project object model” box, as shown in Figure 7-2. Then click OK to dismiss the Trust Center dialog, followed by OK to dismiss the Excel Options dialog.

The next step is to install Xlwings on your computer using a pip install xlwings command or equivalent for your Python environment. Afterward, go to the command line and use the following command to install the Xlwings add-in in Excel:

xlwings addin install

Now create a project directory on your computer and cd to it. Then execute the following command:

xlwings quickstart sentiment
Figure 7-2. Configuring Excel to trust VBA add-ins

This command creates a subdirectory named sentiment in the current directory and initializes it with a pair of files: a spreadsheet named sentiment.xlsm and a Python file named sentiment.py. It is the latter of these in which you will write a UDF that analyzes text for sentiment.

Next, copy sentiment.pkl into the sentiment directory. (If you didn’t generate that file earlier, run the code in Example 7-1 in a Jupyter notebook to generate it now.) Then open sentiment.py in your favorite text editor and replace its contents with the following code. This code loads the sentiment analysis model from the .pkl file and stores a reference to the model in the variable named model. Then it defines a UDF named analyze_text that can be called from Excel. analyze_text vectorizes the text passed to it and inputs it to the model’s predict_proba method. That method returns a number from 0.0 to 1.0, with 0.0 representing negative sentiment and 1.0 representing positive sentiment. When you’re done, save your changes to sentiment.py. The UDF is written. Now it’s time to call it from Excel.

import pickle, os
import xlwings as xw

# Load the model and the vocabulary and create a CountVectorizer
model_path = os.path.abspath(os.path.join(os.path.dirname(__file__),
                             'sentiment.pkl'))

model = pickle.load(open(model_path, 'rb'))

@xw.func
def analyze_text(text):
    score = model.predict_proba([text])[0][1]
    return score

The elegance of Xlwings is that once you’ve written a UDF such as analyze_text, you can call it the same way you call functions built into Excel. But first you must import the UDF. To do that, open sentiment.xlsm in Excel. Go to the “xlwings” tab and click Import Functions to import the analyze_text function, as shown in Figure 7-3. Excel doesn’t tell you if the import was successful, but it does let you know if the import failed—if, for example, you forgot to copy sentiment.pkl into the directory where sentiment.py is stored.

Figure 7-3. Importing the analyze_text function

Type Great food and excellent service into cell A1 (Figure 7-4).

Figure 7-4. Entering a string of text to analyze

Type the following expression into cell B1 to pass the text in cell A1 to the analyze_text function imported from sentiment.py:

=analyze_text(A1)

Confirm that a number from 0.0 to 1.0 appears in cell B1, as shown in Figure 7-5. This is the score that the machine learning model assigned to the text “Great food and excellent service.” Do you agree with the score? Finish up by entering some text strings of your own to see how they score for sentiment.

Figure 7-5. Sentiment analysis in Excel

UDFs written in Python present Excel users with a new whole new world of possibilities thanks to the rich ecosystem of Python libraries available for machine learning, statistical analysis, and other tasks. And they provide a valuable opportunity for Excel users to operationalize machine learning models written in Python.

Summary

Writing a Python client that invokes a Python machine learning model requires little more than an extra line of code to deserialize the model from a .pkl file. One way for a non-Python client to invoke a Python model is to wrap the model in a Python web service and invoke the model using REST APIs. The web service can be hosted locally or in the cloud, and containerizing the web service (and the model) simplifies deployment and makes the software more portable.

An alternative approach is to use ONNX to bridge the language gap. With ONNX, you can save a Scikit model to a .onnx file and load the model from a variety of programming languages, including C, C++, C#, Java, and JavaScript. Once loaded, the model can be invoked just as if it were called from Python.

Another option for invoking machine learning models from non-Python clients is to write the model in the same language as the client. Microsoft’s ML.NET, which is free, cross-platform, and open source, is a great option for C# developers. Other libraries include Java-ML for Java developers and Caret and Tidymodels for R developers. The APIs supported by these libraries are different from the APIs in Scikit, but the principles embodied in them are the same.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.223.33.157