<jupyter_start><jupyter_text>Serving ModelsThis notebook contains a PyTorch model trained on the Iris dataset.  We will be using this model throughout the remainder of the model serving material.<jupyter_code># Install required python packages
%%capture
! pip install mlflow
import numpy as np
import pandas as pd
import torch
import torch.nn as nn
import torch.optim as optim
from torch.nn import functional as F
from sklearn import preprocessing
from sklearn.model_selection import train_test_split
import sklearn
import mlflow
import mlflow.pyfunc
import warnings
warnings.filterwarnings('ignore')
# Mount our gdrive
from google.colab import drive
drive.mount('/content/gdrive', force_remount=True)
# Download the csv to the content directory in colab
# You can see the csv by opening the file explorer tab on the left of the screen
# You may need to click the refresh button at the top of the file explorer window
! wget https://gist.githubusercontent.com/netj/8836201/raw/6f9306ad21398ea43cba4f7d537619d0e07d5ae3/iris.csv<jupyter_output>--2020-03-08 22:11:57--  https://gist.githubusercontent.com/netj/8836201/raw/6f9306ad21398ea43cba4f7d537619d0e07d5ae3/iris.csv
Resolving gist.githubusercontent.com (gist.githubusercontent.com)... 172.16.31.10, 172.16.31.10, 172.16.17.32, ...
Connecting to gist.githubusercontent.com (gist.githubusercontent.com)|172.16.31.10|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 3975 (3.9K) [text/plain]
Saving to: ‘iris.csv’


iris.csv              0%[                    ]       0  --.-KB/s               
iris.csv            100%[===================>]   3.88K  --.-KB/s    in 0s      

2020-03-08 22:11:57 (111 MB/s) - ‘iris.csv’ saved [3975/3975]<jupyter_text>Data Prep<jupyter_code># Load the csv into a pandas dataframe and inspect the data
iris_df = pd.read_csv('/content/iris.csv')
iris_df.head()
# Initialize a label encoder for the class names
# This is an example of a data artifact that will be required at inference time
# Any data preprocessing artifacts should be packaged with the corresponding model
label_encoder = preprocessing.LabelEncoder()

# Convert labels to ints
iris_df['variety'] = label_encoder.fit_transform(iris_df['variety'])
# Label breakdown - we no longer have strings for class names
iris_df['variety'].value_counts()
# Inspect the data after label encoding
print(iris_df.shape)
iris_df.head()
# Drop labels from out training features
iris_x = iris_df.drop('variety', axis = 1)
iris_y = iris_df[['variety']]

# Generate train / test splits for training and evaluation
X_train, x_test, Y_train, y_test = train_test_split(iris_x,
                                                    iris_y,
                                                    test_size=0.3,
                                                    random_state=0)
# Convert to tensors
X_train = torch.from_numpy(X_train.values).float()
X_test = torch.from_numpy(x_test.values).float()
y_train = torch.from_numpy(Y_train.values).view(1,-1)[0]
y_test = torch.from_numpy(y_test.values).view(1,-1)[0]<jupyter_output><empty_output><jupyter_text>Model Models are typically created within a notebook but the model class should be separated out into its own Python file for packaging purposes.  Notebooks are great for experimenting with ideas but it can be a challenge to then take that code and structure it properly in a project format.<jupyter_code># PLEASE WATCH THE ACCOMPANYING VIDEO FOR THIS NOTEBOOK
# NOTE: REMEBER IN THE VIDEO WE SEPERATED OUT OUR MODEL CLASS
# INTO ITS OWN MODEL.PY FILE

# We will eventually pull this model out into its own Python file for packaging

# import torch
# import torch.nn as nn
# from torch.nn import functional as F

# input_size = 4
# output_size = 3
# hidden_size = 30

# class IrisNet(nn.Module):
#     def __init__(self):
#         super(IrisNet, self).__init__()
#         self.fc1 = nn.Linear(input_size, hidden_size)
#         self.fc2 = nn.Linear(hidden_size, hidden_size)
#         self.fc3 = nn.Linear(hidden_size, output_size)

#     def forward(self, X):
#         X = torch.sigmoid((self.fc1(X)))
#         X = torch.sigmoid(self.fc2(X))
#         X = self.fc3(X)

#         return F.log_softmax(X, dim=-1)


# Since we move our model class to model.py
# We can import it from the local file system
# This files is included in the material
from model import IrisNet
# Initialize the network
model = IrisNet()

# Set the optamizer and loss function
optimizer = optim.Adam(model.parameters(), lr = 0.03)
loss_fn = nn.NLLLoss()
# Train the model
epochs = 500

for epoch in range(epochs):
    optimizer.zero_grad()
    y_pred = model(X_train)
    loss = loss_fn(y_pred , y_train)
    loss.backward()
    optimizer.step()

    if epoch % 100 == 0:
        print(f'Epoch: {epoch} loss: {loss.item()}')
def inference(model, user_input):
    """
    Conduct inference for a model
    Args:
      model (torch): An instance of a torch model
      user_input (tensor): User provided input strucutred as a tensor
    Returns:
      Predicted labels [(str)]
    """

    # Get prediction
    pred = torch.argmax(model(user_input), dim=1)

    # Given the predicted integer, find the label from the label encoder
    pred_labels = label_encoder.inverse_transform(pred)

    return pred_labels
# Take a Setosa sample from X_test
example = torch.tensor([[5.1, 3.5, 1.4, 0.2]])

# Inference
pred = inference(model, example)
print(pred)<jupyter_output>['Setosa']<jupyter_text>Serializing data artifactsWe will need to serialize a few data artifacts for this model:1.   Our PyTorch models state_dict2.   The label encoder used for transforming ints to their corresponding strings<jupyter_code>import pickle
# Serialize the label encoder
# This will be required at inference time
le_path = '/content/label_encoder.pkl'
with open(le_path, 'wb') as handle:
    pickle.dump(label_encoder, handle, protocol=pickle.HIGHEST_PROTOCOL)
# Serialize the models state_dict
state_dict_path = f'/content/state_dict.pt'
torch.save(model.state_dict(), state_dict_path)
# Inspect the data artifacts
! ls /content<jupyter_output>gdrive	  label_encoder.pkl  __pycache__  state_dict.pt
iris.csv  model.py	     sample_data<jupyter_text>MLflow PackagingNow that we have everything serialized to disk it's time to package everything up together.<jupyter_code># Here we will create an artifacts object
# It will contain all of the data artifacts that we want to package with the model
artifacts = {
    "state_dict": state_dict_path,
    "label_encoder": le_path
}

# This will serve as an MLflow wrapper for the model
class ModelWrapper(mlflow.pyfunc.PythonModel):

    # Load in the model and all required artifacts
    # The context object is provided by the MLflow framework
    # It will contain all of the artifacts specified above
    def load_context(self, context):
        import torch
        import pickle
        from model import IrisNet

        # Initialize the model and load in the state dict
        self.model = IrisNet()
        self.model.load_state_dict(torch.load(context.artifacts["state_dict"]))

        # Load in and deserialize the label encoder object
        with open(context.artifacts["label_encoder"], 'rb') as handle:
            self.label_encoder = pickle.load(handle)

    # Create a predict function for our models
    def predict(self, context, model_input):
      
        example = torch.tensor(model_input.values)
        pred = torch.argmax(model(example.float()), dim=1)
        pred_labels = self.label_encoder.inverse_transform(pred)
        return pred_labels
# Inspect the default conda environment for MLflow
mlflow.pyfunc.get_default_conda_env()
# PLEASE NOTE: I HAVE CHANGED THE DICT STRUCTURE SLIGHTY FROM THE VIDEO

# Let's create our own conda environment
conda_env = {
    'channels': ['defaults'],
    'dependencies': [
      f'python=3.6.9',
      {
          'pip':[
            f'mlflow=={mlflow.__version__}',
            f'torch=={torch.__version__}',
            f'scikit-learn=={sklearn.__version__}',
            'cloudpickle==1.2.2'
          ]
      }
    ],
    'name': 'mlflow-env'
}
# Location in our gdrive where we want the model to be saved
mlflow_pyfunc_model_path = f"/content/gdrive/My Drive/MLOPS/hands_on/models/iris_model"

# Package the model!
mlflow.pyfunc.save_model(path=mlflow_pyfunc_model_path,
                         python_model=ModelWrapper(),
                         artifacts=artifacts,
                         conda_env=conda_env,
                         code_path=['/content/model.py', '/content/meta_data.txt'])
import pandas as pd

# Load the model in `python_function` format
loaded_model = mlflow.pyfunc.load_model(mlflow_pyfunc_model_path)

# Evaluate the model
test_predictions = loaded_model.predict(pd.DataFrame([[5.1, 3.5, 1.4, 0.2]]))
print(test_predictions)<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.09257168661097313, 'fraction_numerical': 0.024497629261684355, 'mean_word_length': 3.445057701956849, 'pattern_counts': {'":': 2, '<': 18, '<?xml version=': 0, '>': 19, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 4, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 9, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '5', 'dir': 'jupyter-structured-clean-dedup', 'id': '393738', 'license': 'MIT', 'n_tokens_mistral': 2915, 'n_tokens_neox': 2740, 'n_words': 847, 'path': 'MLOps/Module1-Model_Packaging/Inference_Model_Serving/iris_example.ipynb', 'repo_name': 'rcuevass/original-handson-packages', 'size': '8865'}