Melhores Práticas para Artefatos de Modelo

Use essas considerações extras para criação e empacotamento de artefatos de modelo.

Gravando um Arquivo score.py

  • Certifique-se sempre de que os arquivos score.py e runtime.yaml estejam no diretório de nível superior de um artefato de modelo.

    Qualquer outro arquivo que precise fazer parte de um artefato deve estar no mesmo nível que esses dois arquivos ou nos diretórios sob eles:

    .
    |-- runtime.yaml
    |-- score.py
    |-- <your-serialized-models>
  • A implantação de modelo usa as funções score.py para carregar um modelo na memória e fazer previsões.
  • As definições de função, load_model() e predict(), não são editáveis. Somente o corpo dessas funções é personalizável.
  • O caminho permitido para gravar dados no disco ao usar o serviço de implantação de modelo é /home/datascience.
  • Você poderá acessar o Armazenamento de Objetos usando controladores de recursos quando as permissões de identidade do OCI estiverem definidas corretamente para ativá-lo.

Compactando Módulos Personalizados

Qualquer módulo personalizado do qual score.py ou o modelo serializado depende deve ser escrito como scripts Python distintos no mesmo diretório de nível superior que score.py ou menos. Por exemplo, model.joblib depende de uma classe DataFrameLabelEncoder personalizada, que é definida no script dataframelabelencoder.py como neste exemplo:

from category_encoders.ordinal import OrdinalEncoder
from collections import defaultdict
 
from sklearn.base import TransformerMixin
from sklearn.ensemble import RandomForestClassifier
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import LabelEncoder
 
class DataFrameLabelEncoder(TransformerMixin):
    def __init__(self):
        self.label_encoders = defaultdict(LabelEncoder)
         
    def fit(self, X):
        for column in X.columns:
            if X[column].dtype.name  in ["object", "category"]:
                self.label_encoders[column] = OrdinalEncoder()
                self.label_encoders[column].fit(X[column])
        return self
     
    def transform(self, X):
        for column, label_encoder in self.label_encoders.items():
            X[column] = label_encoder.transform(X[column])
        return X

O módulo é então importado pelo arquivo score.py:

"""
   Inference script. This script is used for prediction by scoring server when schema is known.
"""
 
import json
import os
from joblib import load
import io
import pandas as pd
 
from dataframelabelencoder import DataFrameLabelEncoder
 
def load_model():
    """
    Loads model from the serialized format
 
    Returns
    -------
    model:  a model instance on which predict API can be invoked
    """
    model_dir = os.path.dirname(os.path.realpath(__file__))
    contents = os.listdir(model_dir)
    model_file_name = "model.joblib"
    # TODO: Load the model from the model_dir using the appropriate loader
    # Below is a sample code to load a model file using `cloudpickle` which was serialized using `cloudpickle`
    # from cloudpickle import cloudpickle
    if model_file_name in contents:
        with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), model_file_name), "rb") as file:
            model = load(file) # Use the loader corresponding to your model file.
    else:
        raise Exception('{0} is not found in model directory {1}'.format(model_file_name, model_dir))
     
    return model
 
 
def predict(data, model=load_model()) -> dict:
    """
    Returns prediction given the model and data to predict
 
    Parameters
    ----------
    model: Model instance returned by load_model API
    data: Data format as expected by the predict API of the core estimator. For eg. in case of sckit models it could be numpy array/List of list/Panda DataFrame
 
    Returns
    -------
    predictions: Output from scoring server
        Format: { 'prediction': output from `model.predict` method }
 
    """
    assert model is not None, "Model is not loaded"
    X = pd.read_json(io.StringIO(data)) if isinstance(data, str) else pd.DataFrame.from_dict(data)
    preds = model.predict(X).tolist()  
    return { 'prediction': preds }

No exemplo anterior, a estrutura de artefato deve ser:


.
|-- score.py 
|-- dataframelabelencoder.py 
|-- model.joblib 
|-- runtime.yaml

Alterando assinaturas load_model() e forecast()

A função predict(data, model=load_model()) espera os dados do payload e um objeto de modelo e é retornada por load_model() por padrão. Seu caso de uso pode exigir que um parâmetro extra seja passado para predict(). Um exemplo é um escalador ou uma tabela de lookup. Você poderá adicionar parâmetros a essa função se o parâmetro adicionado tiver um valor padrão designado.

No exemplo a seguir, as previsões dependem de um modelo de PCA e de um objeto escalador. Mostra como predict() pode usar um parâmetro extra chamado scaler. Por padrão, a função load_scaler() retorna um valor para o parâmetro scaler. Recomendamos que você siga esse padrão. Se predict() ou load_model() exigir parâmetros extras, eles deverão ser definidos com valores padrão retornados por funções definidas em score.py. Este exemplo adiciona parâmetros a predict():

import json
import os
from cloudpickle import cloudpickle
model_pickle_name = 'pca.pkl'
scaler_pickle_name = 'scaler.pkl'
"""
   Inference script. This script is used for prediction by scoring server when schema is known.
"""
def load_model(model_file_name=model_pickle_name):
    """
    Loads model from the serialized format
    Returns
    -------
    model:  a model instance on which predict API can be invoked
    """
    model_dir = os.path.dirname(os.path.realpath(__file__))
    contents = os.listdir(model_dir)
    if model_file_name in contents:
        with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), model_file_name), "rb") as file:
            return cloudpickle.load(file)
    else:
        raise Exception('{0} is not found in model directory {1}'.format(model_file_name, model_dir))
 
def load_scaler(model_file_name=scaler_pickle_name):
    """
    Loads model from the serialized format
    Returns
    -------
    model:  a model instance on which predict API can be invoked
    """
    model_dir = os.path.dirname(os.path.realpath(__file__))
    contents = os.listdir(model_dir)
    if model_file_name in contents:
        with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), model_file_name), "rb") as file:
            return cloudpickle.load(file)
    else:
        raise Exception('{0} is not found in model directory {1}'.format(model_file_name, model_dir))
 
def predict(data, model=load_model(), scaler=load_scaler()):
    """
    Returns prediction given the model and data to predict
    Parameters
    ----------
    model: Model instance returned by load_model API
    data: Data format as expected by the predict API of the core estimator. For eg. in case of sckit models it could be numpy array/List of list/Panda DataFrame
    Returns
    -------
    predictions: Output from scoring server
        Format: {'prediction':output from model.predict method}
    """
    from pandas import read_json, DataFrame
    from io import StringIO
    X = read_json(StringIO(data)) if isinstance(data, str) else DataFrame.from_dict(data)
    X_s = scaler.transform(X)
    return {'prediction':model.transform(X_s).tolist()[0]}

Testando um Artefato de Modelo Antes de Salvar

Antes de salvar um modelo no catálogo, recomendamos que você teste o artefato completamente. Este trecho de código para testar um artefato de modelo antes de salvá-lo no catálogo:

  • Altera o caminho do Python inserindo o caminho para o artefato de modelo.

  • Carrega o modelo na memória usando load_model().

  • Por fim, chama o predict().

Antes de executar o snippet de código, crie um novo arquivo de notebook em uma sessão de notebook, altere o kernel e selecione o mesmo ambiente conda que deseja usar para implantação de modelo (ambiente conda de inferência). Copie e cole o trecho de código e execute o código.

Exemplo de Código do Teste de Artefato de Modelo

import sys
from json import dumps
 
 
# The local path to your model artifact directory is added to the Python path.
# replace <your-model-artifact-path>
sys.path.insert(0, f"<your-model-artifact-path>")
 
# importing load_model() and predict() that are defined in score.py
from score import load_model, predict
 
# Loading the model to memory
_ = load_model()
# Making predictions on a JSON string object (dumps(data)). Here we assume
# that predict() is taking data in JSON format
predictions_test = predict(dumps(data), _)
predictions_test

O método predictions_test contém as previsões feitas pelo modelo no payload de string JSON de amostra data. Compare predictions_test com um resultado de modelo conhecido de um conjunto de dados específico. Por exemplo, data pode ser uma amostra do conjunto de dados de treinamento.

Exemplo de Previsão de Imagem

O método score.py predict() trata o processamento da solicitação recebida como neste arquivo score.py de exemplo:

"""
   Inference script. This script is used for prediction by scoring server when schema is known.
"""
import torch
import torchvision
import io
import numpy as np
from PIL import Image
import os
 
 
# COCO Labels
COCO_INSTANCE_CATEGORY_NAMES = [
    '__background__', 'person', 'bicycle', 'car', 'motorcycle']
 
 
model_name = 'PyTorch_Retinanet.pth'
def load_model(model_file_name = model_name):
    """
    Loads model from the serialized format
 
    Returns
    -------
    model:  Pytorch model instance
    """
    model = torchvision.models.detection.retinanet_resnet50_fpn(pretrained=False, pretrained_backbone=False)
    cur_dir = os.path.dirname(os.path.abspath(__file__))
    model.load_state_dict(torch.load(os.path.join(cur_dir, model_file_name)))
    model.eval()
    return model
 
 
def predict(data, model=load_model()):
    """
    Returns prediction given the model and data to predict
 
    Parameters
    ----------
    model: Model instance returned by load_model API
    data: Data format in json
 
    Returns
    -------
    predictions: Output from scoring server
        Format: {'prediction':output from model.predict method}
 
    """
    img_bytes = io.BytesIO(data)
    image = Image.open(img_bytes)
    image_np = np.asarray(image)
    image_th = torch.from_numpy(image_np)
    image_th = image_th.permute(2, 0, 1)
    image_th = image_th.unsqueeze(0) / 255
    with torch.no_grad():
        pred = model(image_th)
    object_index_list = np.argwhere(pred[0].get("scores") > 0.5)
    label_index_list = pred[0].get("labels")
    labels = [COCO_INSTANCE_CATEGORY_NAMES[label_index_list[i]] for i in object_index_list]
    box_list = pred[0].get("boxes")
    boxes = [box_list[i].numpy().tolist() for i in object_index_list][0]
    return {'prediction': {
        'labels': labels,
        'boxes': boxes,
    }}