Effectuez des prédictions avec ONNX sur des modèles de vision par ordinateur à parti d’AutoML

S’APPLIQUE À : Kit de développement logiciel (SDK) Python azure-ai-mlv2 (préversion)

Dans cet article, vous apprenez à utiliser ONNX (Open Neural Network Exchange) pour effectuer des prédictions sur des modèles de vision par ordinateur générés par le ML automatisé (AutoML) dans Azure Machine Learning.

Pour utiliser ONNX pour les prédictions, vous devez :

  1. Télécharger les fichiers de modèle ONNX à partir d’une exécution d’entraînement AutoML.
  2. Comprendre les entrées et les sorties d’un modèle ONNX.
  3. Prétraiter vos données afin qu’elles soient au format requis pour les images d’entrée.
  4. Procéder à l’inférence avec le runtime ONNX pour Python.
  5. Visualiser les prédictions de la détection d’objets et les tâches de segmentation d’instances.

ONNX est une norme ouverte pour les modèles de machine learning et de deep learning. Elle permet l’importation et l’exportation (interopérabilité) des modèles sur les infrastructures d’IA populaires. Pour plus de détails, explorez le projet ONNX GitHub.

Le Runtime ONNX est un projet open source qui prend en charge l’inférence multiplateforme. Le runtime ONNX fournit des API sur des langages de programmation (notamment Python, C++, C#, C, Java et JavaScript). Vous pouvez utiliser ces API pour effectuer une inférence sur les images d’entrée. Une fois que le modèle a été exporté au format ONNX, vous pouvez utiliser ces API sur n’importe quel langage de programmation dont votre projet a besoin.

Dans ce guide, vous apprenez à utiliser les API Python pour le runtime ONNX afin d’effectuer des prédictions sur des images pour des tâches de vision populaires. Vous pouvez utiliser ces modèles exportés par ONNX dans différents langages.

Prérequis

Télécharger les fichiers de modèle ONNX

Vous pouvez télécharger les fichiers de modèle ONNX à partir d’une exécution AutoML à l’aide de l’interface utilisateur Azure Machine Learning studio ou du kit de développement logiciel (SDK) Python Azure Machine Learning. Nous vous recommandons de les télécharger via le kit de développement logiciel (SDK) avec le nom de l’expérience et l’ID d’exécution parent.

Azure Machine Learning Studio

Dans Azure Machine Learning studio, accédez à votre expérience en utilisant le lien hypertexte vers l’expérience générée dans le notebook de formation, ou en sélectionnant le nom de l’expérience dans l’onglet Expériences sous Ressources. Sélectionnez ensuite la meilleure exécution enfant.

Dans la meilleure exécution enfant, accédez à Sorties + journaux>train_artifacts. Utilisez le bouton Télécharger pour télécharger manuellement les fichiers suivants :

  • labels.json : fichier qui contient toutes les classes ou étiquettes du jeu de données d’apprentissage.
  • model.onnx : modèle au format ONNX.

Screenshot that shows selections for downloading O N N X model files.

Enregistrez les fichiers de modèle téléchargés dans un répertoire. L’exemple de cet article utilise le répertoire ./automl_models.

SDK Python Azure Machine Learning

Avec le kit de développement logiciel (SDK), vous pouvez sélectionner la meilleure expérience enfant (par métrique principale) avec le nom de l’expérience et l’ID d’exécution parent. Ensuite, vous pouvez télécharger les fichiers labels.json et model.onnx.

Le code suivant retourne la meilleure expérience enfant en fonction de la métrique principale pertinente.

from azure.identity import DefaultAzureCredential
from azure.ai.ml import MLClient
mlflow_client = MlflowClient()

credential = DefaultAzureCredential()
ml_client = None
try:
    ml_client = MLClient.from_config(credential)
except Exception as ex:
    print(ex)
    # Enter details of your Azure Machine Learning workspace
    subscription_id = ''   
    resource_group = ''  
    workspace_name = ''
    ml_client = MLClient(credential, subscription_id, resource_group, workspace_name)
import mlflow
from mlflow.tracking.client import MlflowClient

# Obtain the tracking URL from MLClient
MLFLOW_TRACKING_URI = ml_client.workspaces.get(
    name=ml_client.workspace_name
).mlflow_tracking_uri

mlflow.set_tracking_uri(MLFLOW_TRACKING_URI)

# Specify the job name
job_name = ''

# Get the parent run
mlflow_parent_run = mlflow_client.get_run(job_name)
best_child_run_id = mlflow_parent_run.data.tags['automl_best_child_run_id']
# get the best child run
best_run = mlflow_client.get_run(best_child_run_id)

Téléchargez le fichier labels.json qui contient toutes les classes et étiquettes du jeu de données d’apprentissage.

local_dir = './automl_models'
if not os.path.exists(local_dir):
    os.mkdir(local_dir)

labels_file = mlflow_client.download_artifacts(
    best_run.info.run_id, 'train_artifacts/labels.json', local_dir
)

Téléchargez le fichier model.onnx.

onnx_model_path = mlflow_client.download_artifacts(
    best_run.info.run_id, 'train_artifacts/model.onnx', local_dir
)

En cas d’inférence par lots pour la détection d’objets et la segmentation d’instance à l’aide de modèles ONNX, reportez-vous à la section relative à la génération de modèles pour le scoring par lots.

Génération de modèle pour la notation par lots

Par défaut, AutoML pour les images prend en charge le scoring par lots pour la classification. Mais les modèles ONNX de détection d’objets et de segmentation d’instances ne prennent pas en charge l’inférence par lots. En cas d’inférence par lots pour la détection d’objets et la segmentation d’instances, utilisez la procédure suivante pour générer un modèle ONNX pour la taille de lot requise. Les modèles générés pour une taille de lot spécifique ne fonctionnent pas pour d’autres tailles de lot.

Téléchargez le fichier d’environnement conda et créez un objet d’environnement à utiliser avec le travail de commande.

#  Download conda file and define the environment

conda_file = mlflow_client.download_artifacts(
    best_run.info.run_id, "outputs/conda_env_v_1_0_0.yml", local_dir
)
from azure.ai.ml.entities import Environment
env = Environment(
    name="automl-images-env-onnx",
    description="environment for automl images ONNX batch model generation",
    image="mcr.microsoft.com/azureml/openmpi4.1.0-cuda11.1-cudnn8-ubuntu18.04",
    conda_file=conda_file,
)

Utilisez les arguments spécifiques au modèle suivants pour soumettre le script. Pour plus d’informations sur les arguments, consultez les hyperparamètres spécifiques au modèle et pour connaître les noms de modèle de détection d’objets pris en charge, consultez la section sur les architectures de modèle prises en charge.

Pour récupérer les valeurs d’argument nécessaires à la création du modèle de notation par lots, consultez les scripts de notation générés dans le dossier des sorties des exécutions de formation AutoML. Utilisez les valeurs d’hyperparamètres disponibles dans la variable de paramètres de modèle dans le fichier de notation pour la meilleure exécution enfant.

Pour la classification d’images multiclasse, le modèle ONNX généré pour la meilleure exécution enfant prend en charge la notation par lots par défaut. Par conséquent, aucun argument spécifique au modèle n’est nécessaire pour ce type de tâche et vous pouvez passer à la section Charger les étiquettes et les fichiers de modèle ONNX.

Téléchargez et conservez le fichier ONNX_batch_model_generator_automl_for_images.py dans le répertoire actif pour envoyer le script. Utilisez le travail de commande suivant pour envoyer le script ONNX_batch_model_generator_automl_for_images.py disponible dans le référentiel GitHub azureml-examples, afin de générer un modèle ONNX d’une taille de lot spécifique. Dans le code suivant, l’environnement de modèle entraîné est utilisé pour envoyer ce script afin de générer et d’enregistrer le modèle ONNX dans le répertoire des sorties.

Pour la classification d’images multiclasse, le modèle ONNX généré pour la meilleure exécution enfant prend en charge la notation par lots par défaut. Par conséquent, aucun argument spécifique au modèle n’est nécessaire pour ce type de tâche et vous pouvez passer à la section Charger les étiquettes et les fichiers de modèle ONNX.

Une fois le modèle de lot généré, téléchargez-le manuellement via l’interface utilisateur à partir de Sorties+journaux>sorties, ou utilisez la méthode suivante :

batch_size = 8  # use the batch size used to generate the model
returned_job_run = mlflow_client.get_run(returned_job.name)

# Download run's artifacts/outputs
onnx_model_path = mlflow_client.download_artifacts(
    returned_job_run.info.run_id, 'outputs/model_'+str(batch_size)+'.onnx', local_dir
)

Après l’étape de téléchargement du modèle, utilisez le package Python du runtime ONNX pour effectuer une inférence à l’aide du fichier model.onnx. À des fins de démonstration, cet article utilise les jeux de données de la rubrique Comment préparer les jeux de données d’images pour chaque tâche de vision.

Nous avons entraîné les modèles pour toutes les tâches de vision avec leurs jeux de données respectifs afin d’illustrer l’inférence de modèle ONNX.

Charger les étiquettes et les fichiers de modèle ONNX

L’extrait de code suivant charge labels.json, dans lequel les noms de classe sont triés. Autrement dit, si le modèle ONNX prédit un ID d’étiquette de 2, il correspond au nom d’étiquette donné au troisième index du fichier labels.json.

import json
import onnxruntime

labels_file = "automl_models/labels.json"
with open(labels_file) as f:
    classes = json.load(f)
print(classes)
try:
    session = onnxruntime.InferenceSession(onnx_model_path)
    print("ONNX model loaded...")
except Exception as e: 
    print("Error loading ONNX file: ", str(e))

Obtenir les détails d’entrée et de sortie attendus d’un modèle ONNX

Une fois que vous disposez du modèle, il est important de connaître certains détails spécifiques aux modèles et aux tâches. Ces détails incluent le nombre d’entrées et le nombre de sorties, la forme d’entrée ou le format attendu pour le prétraitement de l’image, ainsi que la forme de sortie, afin de connaître les sorties spécifiques à un modèle ou à une tâche.

sess_input = session.get_inputs()
sess_output = session.get_outputs()
print(f"No. of inputs : {len(sess_input)}, No. of outputs : {len(sess_output)}")

for idx, input_ in enumerate(range(len(sess_input))):
    input_name = sess_input[input_].name
    input_shape = sess_input[input_].shape
    input_type = sess_input[input_].type
    print(f"{idx} Input name : { input_name }, Input shape : {input_shape}, \
    Input type  : {input_type}")  

for idx, output in enumerate(range(len(sess_output))):
    output_name = sess_output[output].name
    output_shape = sess_output[output].shape
    output_type = sess_output[output].type
    print(f" {idx} Output name : {output_name}, Output shape : {output_shape}, \
    Output type  : {output_type}") 

Formats d’entrée et de sortie attendus pour le modèle ONNX

Chaque modèle ONNX a un ensemble prédéfini de formats d’entrée et de sortie.

Cet exemple applique le modèle formé sur le jeu de données fridgeObjects de 134 images et 4 classes/étiquettes pour expliquer l’inférence de modèle ONNX. Pour plus d’informations sur l’apprentissage d’une tâche de classification d’images, consultez le notebook d’images multiclasses.

Format d’entrée

L’entrée est une image prétraitée.

Nom d’entrée Forme d’entrée Type d’entrée Description
input1 (batch_size, num_channels, height, width) ndarray(float) L’entrée est une image prétraitée, avec la forme (1, 3, 224, 224) pour une taille de lot de 1, et une hauteur et une largeur de 224. Ces chiffres correspondent aux valeurs utilisées pour crop_size dans l’exemple d’apprentissage.

Format de sortie

La sortie est un tableau de logits pour toutes les classes/étiquettes.

Nom de sortie Forme de sortie Type de sortie Description
output1 (batch_size, num_classes) ndarray(float) Le modèle retourne logits (sans softmax). Par exemple, pour les classes de taille de lot 1 et 4, il retourne (1, 4).

Prétraitement

Effectuez les étapes de prétraitement suivantes pour l’inférence de modèle ONNX :

  1. Convertissez l’image en RVB.
  2. Redimensionnez l’image avec les valeurs valid_resize_size et valid_resize_size qui correspondent aux valeurs utilisées dans la transformation du jeu de données de validation au cours de l’apprentissage. La valeur par défaut pour valid_resize_size est 256.
  3. Rognez et centrez l’image sur height_onnx_crop_size et width_onnx_crop_size. Cela correspond à valid_crop_size avec la valeur par défaut de 224.
  4. Remplacez HxWxC par CxHxW.
  5. Convertissez au type float.
  6. Normalisez avec mean = [0.485, 0.456, 0.406] et std = [0.229, 0.224, 0.225] d’ImageNet.

Si vous avez choisi des valeurs différentes pour les hyperparamètresvalid_resize_size et valid_crop_size pendant la formation, ces valeurs doivent être utilisées.

Obtenez la forme d’entrée nécessaire pour le modèle ONNX.

batch, channel, height_onnx_crop_size, width_onnx_crop_size = session.get_inputs()[0].shape
batch, channel, height_onnx_crop_size, width_onnx_crop_size

Sans PyTorch

import glob
import numpy as np
from PIL import Image

def preprocess(image, resize_size, crop_size_onnx):
    """Perform pre-processing on raw input image
    
    :param image: raw input image
    :type image: PIL image
    :param resize_size: value to resize the image
    :type image: Int
    :param crop_size_onnx: expected height of an input image in onnx model
    :type crop_size_onnx: Int
    :return: pre-processed image in numpy format
    :rtype: ndarray 1xCxHxW
    """

    image = image.convert('RGB')
    # resize
    image = image.resize((resize_size, resize_size))
    #  center  crop
    left = (resize_size - crop_size_onnx)/2
    top = (resize_size - crop_size_onnx)/2
    right = (resize_size + crop_size_onnx)/2
    bottom = (resize_size + crop_size_onnx)/2
    image = image.crop((left, top, right, bottom))

    np_image = np.array(image)
    # HWC -> CHW
    np_image = np_image.transpose(2, 0, 1) # CxHxW
    # normalize the image
    mean_vec = np.array([0.485, 0.456, 0.406])
    std_vec = np.array([0.229, 0.224, 0.225])
    norm_img_data = np.zeros(np_image.shape).astype('float32')
    for i in range(np_image.shape[0]):
        norm_img_data[i,:,:] = (np_image[i,:,:]/255 - mean_vec[i])/std_vec[i]
             
    np_image = np.expand_dims(norm_img_data, axis=0) # 1xCxHxW
    return np_image

# following code loads only batch_size number of images for demonstrating ONNX inference
# make sure that the data directory has at least batch_size number of images

test_images_path = "automl_models_multi_cls/test_images_dir/*" # replace with path to images
# Select batch size needed
batch_size = 8
# you can modify resize_size based on your trained model
resize_size = 256
# height and width will be the same for classification
crop_size_onnx = height_onnx_crop_size 

image_files = glob.glob(test_images_path)
img_processed_list = []
for i in range(batch_size):
    img = Image.open(image_files[i])
    img_processed_list.append(preprocess(img, resize_size, crop_size_onnx))
    
if len(img_processed_list) > 1:
    img_data = np.concatenate(img_processed_list)
elif len(img_processed_list) == 1:
    img_data = img_processed_list[0]
else:
    img_data = None

assert batch_size == img_data.shape[0]

Avec PyTorch

import glob
import torch
import numpy as np
from PIL import Image
from torchvision import transforms

def _make_3d_tensor(x) -> torch.Tensor:
    """This function is for images that have less channels.

    :param x: input tensor
    :type x: torch.Tensor
    :return: return a tensor with the correct number of channels
    :rtype: torch.Tensor
    """
    return x if x.shape[0] == 3 else x.expand((3, x.shape[1], x.shape[2]))

def preprocess(image, resize_size, crop_size_onnx):
    transform = transforms.Compose([
        transforms.Resize(resize_size),
        transforms.CenterCrop(crop_size_onnx),
        transforms.ToTensor(),
        transforms.Lambda(_make_3d_tensor),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])])
    
    img_data = transform(image)
    img_data = img_data.numpy()
    img_data = np.expand_dims(img_data, axis=0)
    return img_data

# following code loads only batch_size number of images for demonstrating ONNX inference
# make sure that the data directory has at least batch_size number of images

test_images_path = "automl_models_multi_cls/test_images_dir/*"  # replace with path to images
# Select batch size needed
batch_size = 8
# you can modify resize_size based on your trained model
resize_size = 256
# height and width will be the same for classification
crop_size_onnx = height_onnx_crop_size 

image_files = glob.glob(test_images_path)
img_processed_list = []
for i in range(batch_size):
    img = Image.open(image_files[i])
    img_processed_list.append(preprocess(img, resize_size, crop_size_onnx))
    
if len(img_processed_list) > 1:
    img_data = np.concatenate(img_processed_list)
elif len(img_processed_list) == 1:
    img_data = img_processed_list[0]
else:
    img_data = None

assert batch_size == img_data.shape[0]

Inférence avec le runtime ONNX

L’inférence avec le runtime ONNX diffère pour chaque tâche de vision par ordinateur.

def get_predictions_from_ONNX(onnx_session, img_data):
    """Perform predictions with ONNX runtime
    
    :param onnx_session: onnx model session
    :type onnx_session: class InferenceSession
    :param img_data: pre-processed numpy image
    :type img_data: ndarray with shape 1xCxHxW
    :return: scores with shapes
            (1, No. of classes in training dataset) 
    :rtype: numpy array
    """

    sess_input = onnx_session.get_inputs()
    sess_output = onnx_session.get_outputs()
    print(f"No. of inputs : {len(sess_input)}, No. of outputs : {len(sess_output)}")    
    # predict with ONNX Runtime
    output_names = [ output.name for output in sess_output]
    scores = onnx_session.run(output_names=output_names,\
                                               input_feed={sess_input[0].name: img_data})
    
    return scores[0]

scores = get_predictions_from_ONNX(session, img_data)

Post-traitement

Appliquez softmax() à des valeurs prédites pour obtenir des scores de confiance de classification (probabilités) pour chaque classe. La prédiction sera la classe ayant la probabilité la plus élevée.

Sans PyTorch

def softmax(x):
    e_x = np.exp(x - np.max(x, axis=1, keepdims=True))
    return e_x / np.sum(e_x, axis=1, keepdims=True)

conf_scores = softmax(scores)
class_preds = np.argmax(conf_scores, axis=1)
print("predicted classes:", ([(class_idx, classes[class_idx]) for class_idx in class_preds]))

Avec PyTorch

conf_scores = torch.nn.functional.softmax(torch.from_numpy(scores), dim=1)
class_preds = torch.argmax(conf_scores, dim=1)
print("predicted classes:", ([(class_idx.item(), classes[class_idx]) for class_idx in class_preds]))

Visualiser des prédictions

Visualiser une image d’entrée avec des étiquettes.

import matplotlib.image as mpimg
import matplotlib.pyplot as plt
%matplotlib inline

sample_image_index = 0 # change this for an image of interest from image_files list
IMAGE_SIZE = (18, 12)
plt.figure(figsize=IMAGE_SIZE)
img_np = mpimg.imread(image_files[sample_image_index])

img = Image.fromarray(img_np.astype('uint8'), 'RGB')
x, y = img.size

fig,ax = plt.subplots(1, figsize=(15, 15))
# Display the image
ax.imshow(img_np)

label = class_preds[sample_image_index]
if torch.is_tensor(label):
    label = label.item()
    
conf_score = conf_scores[sample_image_index]
if torch.is_tensor(conf_score):
    conf_score = np.max(conf_score.tolist())
else:
    conf_score = np.max(conf_score)

display_text = '{} ({})'.format(label, round(conf_score, 3))
print(display_text)

color = 'red'
plt.text(30, 30, display_text, color=color, fontsize=30)

plt.show()

Étapes suivantes