AutoML のコンピューター ビジョン モデルで ONNX を使用して予測を行う

適用対象: Python SDK azure-ai-ml v2 (現行)

この記事では、Open Neural Network Exchange (ONNX) を使用して、Azure Machine Learning の自動機械学習 (AutoML) から生成されたコンピューター ビジョン モデルで予測を行う方法について説明します。

予測に ONNX を使用するには、次のことを行う必要があります。

  1. AutoML トレーニングの実行から ONNX モデル ファイルをダウンロードします。
  2. ONNX モデルの入力と出力を理解します。
  3. 入力画像に必要な形式になるようにデータを前処理します。
  4. Python 用の ONNX ランタイムで推論を実行します。
  5. 物体検出とインスタンスのセグメント化タスクの予測を視覚化します。

ONNX は、機械学習とディープ ラーニング モデルのオープン スタンダードです。 これにより、一般的な AI フレームワーク間でモデルのインポートとエクスポート (相互運用性) が可能になります。 詳細については、ONNX GitHub プロジェクトを参照してください。

ONNX Runtime は、クロスプラットフォームの推論をサポートするオープン ソース プロジェクトです。 ONNX ランタイムでは、複数のプログラミング言語 (Python、C++、C#、C、Java、JavaScript など) にわたって API が提供されます。 これらの API を使用して、入力画像に対する推論を実行できます。 ONNX 形式でエクスポートされたモデルの作成後、プロジェクトに必要な任意のプログラミング言語でこれらの API を使用できます。

このガイドでは、ONNX Runtime 用の Python API を使用して、一般的なビジョン タスクの画像で予測を行う方法について説明します。 これらの ONNX のエクスポートされたモデルは、複数の言語で使用できます。

前提条件

ONNX モデル ファイルをダウンロードする

ONNX モデル ファイルは、Azure Machine Learning スタジオ UI または Azure Machine Learning Python SDK を使用して、AutoML 実行からダウンロードできます。 SDK を通じて実験名と親実行 ID を指定してダウンロードすることをお勧めします。

Azure Machine Learning Studio

Azure Machine Learning スタジオで、トレーニング ノートブックで生成された実験へのハイパーリンクを使用するか、[アセット] の下の [実験] タブで実験名を選択して、実験に移動します。 次に、最適な子の実行を選択します。

最適な子を実行するには、[出力+ログ>train_artifacts]にアクセスします。 [ダウンロード] ボタンを使用して、次のファイルを手動でダウンロードします。

  • labels.json: トレーニング データセット内のすべてのクラスまたはラベルを含むファイル。
  • model.onnx: ONNX 形式のモデル。

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

ダウンロードしたモデル ファイルをディレクトリに保存します。 この記事の例では、./automl_models ディレクトリを使用します。

Azure Machine Learning Python SDK

SDK では、実験名と親実行 ID を使用して、最適な子実行を選択できます (プライマリ メトリックごと)。 次に、labels.json および model.onnx ファイルをダウンロードできます。

次のコードでは、該当するプライマリ メトリックに基づいて、最適な子実行が返されます。

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)

トレーニング データセット内のすべてのクラスおよびラベルを含む labels.json ファイルをダウンロードします。

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
)

model.onnx ファイルをダウンロードします。

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

ONNX モデルを使用する物体検出とインスタンス セグメント化のバッチ推論の場合は、「バッチ スコアリングのモデル生成」セクションを参照してください。

バッチ スコアリングのモデル生成

既定では、AutoML for Images で分類のバッチ スコアリングがサポートされます。 ただし、物体検出とインスタンス セグメント化の ONNX モデルでは、バッチ推論がサポートされません。 物体検出とインスタンスのセグメント化に対してバッチ推論を行う場合は、次の手順を使用して、必要なバッチ サイズの ONNX モデルを生成します。 特定のバッチ サイズに対して生成されたモデルは、他のバッチ サイズでは機能しません。

conda 環境ファイルをダウンロードし、コマンド ジョブで使用される環境オブジェクトを作成します。

#  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,
)

次のモデル固有の引数を使用して、スクリプトを送信します。 引数の詳細については、モデル固有のハイパーパラメーターに関するセクションを参照してください。サポートされる物体検出モデルの名前については、「サポートされているモデル アーキテクチャ」セクションを参照してください。

バッチ スコアリング モデルの作成に必要な引数の値を取得するには、AutoML トレーニング実行の出力フォルダーの下に生成されるスコアリング スクリプトを参照してください。 最適な子実行には、スコアリング ファイル内のモデル設定変数で使用できるハイパーパラメーター値を使用します。

複数クラスの画像分類の場合、最適な子実行に対して生成される ONNX モデルでは、既定でバッチ スコアリングがサポートされます。 そのため、このタスクの種類には、モデル固有の引数は必要ありません。「ラベルと ONNX モデル ファイルを読み込む」セクションに進むことができます。

スクリプトを送信するために、現在のディレクトリに ONNX_batch_model_generator_automl_for_images.py ファイルをダウンロードして保存します。 次のコマンド ジョブを使用して、azureml-examples GitHub リポジトリにある azureml-examples GitHub リポジトリにあるスクリプト ONNX_batch_model_generator_automl_for_images.py を送信し、特定のバッチ サイズの ONNX モデルを生成します。 次のコードでは、トレーニング済みのモデル環境を使用してこのスクリプトを送信し、ONNX モデルを生成して出力ディレクトリに保存します。

複数クラスの画像分類の場合、最適な子実行に対して生成される ONNX モデルでは、既定でバッチ スコアリングがサポートされます。 そのため、このタスクの種類には、モデル固有の引数は必要ありません。「ラベルと ONNX モデル ファイルを読み込む」セクションに進むことができます。

バッチ モデルが生成されたら、それを UI ([出力 + ログ]>[出力]) から手動でダウンロードするか、次のメソッドを使用します。

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
)

モデルのダウンロードの手順の後、ONNX Runtime Python パッケージを使用し、model.onnx ファイルを利用して推論を実行します。 デモンストレーションを目的として、この記事では各ビジョン タスクのために、画像データセットの準備方法に関するページのデータセットを使用します。

ONNX モデルの推論のデモンストレーションをするために、各データセットを使用してすべてのビジョン タスクのモデルをトレーニングしました。

ラベルと ONNX モデル ファイルを読み込む

次のコード スニペットでは、クラス名が順序付けられている labels.json が読み込まれます。 つまり、ONNX モデルでラベル ID が 2 であると予測される場合は、labels.json ファイルで 3 番目のインデックスに指定されているラベル名が対応しています。

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))

ONNX モデルの予想される入力と出力の詳細を取得する

モデルがある場合、モデル固有の詳細とタスク固有の詳細を把握しておくことが重要です。 これらの詳細には、モデル固有の出力またはタスク固有の出力がわかるように、入力の数と出力の数、画像の前処理のために想定される入力の図形または形式、および出力の図形が含まれます。

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}") 

ONNX モデルで想定される入力形式と出力形式

すべての ONNX モデルには、入力形式と出力形式の定義済みセットがあります。

この例では、ONNX モデルの推論を説明するために、134 個の画像と 4 つのクラスおよびラベルの fridgeObjects データセットでトレーニングされたモデルを適用します。 画像分類タスクのトレーニングの詳細については、複数クラスの画像分類のノートブックを参照してください。

入力形式

入力は、前処理された画像です。

入力名 入力図形 入力型 説明
input1 (batch_size, num_channels, height, width) ndarray(float) 入力は前処理された画像であり、図形は (1, 3, 224, 224) で、バッチ サイズが 1、高さと幅が 224 です。 これらの数値は、トレーニングの例で crop_size に使用されている値と対応しています。

出力形式

出力は、すべてのクラスおよびラベルのロジットの配列です。

出力名 出力図形 出力の種類 説明
output1 (batch_size, num_classes) ndarray(float) モデルはロジットを返します (softmax なし)。 たとえば、バッチ サイズが 1 で 4 クラスの場合は、(1, 4) が返されます。

前処理

ONNX モデルの推論のために、次の前処理手順を実行します。

  1. 画像を RGB に変換します。
  2. 画像のサイズを、トレーニング中の検証データセットの変換に使用される値に対応する、valid_resize_sizevalid_resize_size の値に変更します。 valid_resize_size の既定値は 256 です。
  3. 画像を height_onnx_crop_sizewidth_onnx_crop_size に中央でトリミングします。 これは、既定値が 224 である valid_crop_size に対応しています。
  4. HxWxCCxHxW に変更します。
  5. float 型に変換します。
  6. ImageNet の mean = [0.485, 0.456, 0.406] および std = [0.229, 0.224, 0.225] を使用して正規化します。

トレーニング中にハイパーパラメーターvalid_resize_sizevalid_crop_size に異なる値を選択した場合は、それらの値を使用する必要があります。

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

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]

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]

ONNX Runtime を使用した推論

ONNX Runtime を使用した推論は、コンピューター ビジョン タスクごとに異なります。

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)

後処理

予測値に softmax() を適用して、各クラスの分類信頼度スコア (確率) を取得します。 その後、予測は確率が最も高いクラスになります。

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]))

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]))

予測の視覚化

ラベル付きの入力イメージを視覚化します。

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()

次のステップ