diff --git a/cli/foundation-models/system/finetune/image-classification/multiclass-classification/hftransformers-fridgeobjects-multiclass-classification.sh b/cli/foundation-models/system/finetune/image-classification/multiclass-classification/hftransformers-fridgeobjects-multiclass-classification.sh index cee5fbc773..e685885ccd 100644 --- a/cli/foundation-models/system/finetune/image-classification/multiclass-classification/hftransformers-fridgeobjects-multiclass-classification.sh +++ b/cli/foundation-models/system/finetune/image-classification/multiclass-classification/hftransformers-fridgeobjects-multiclass-classification.sh @@ -22,7 +22,7 @@ huggingface_model_name="microsoft/beit-base-patch16-224-pt22k-ft22k" # This is the foundation model for finetuning from azureml system registry # using the latest version of the model - not working yet aml_registry_model_name="microsoft-beit-base-patch16-224-pt22k-ft22k" -model_version=1 +model_label="latest" version=$(date +%s) finetuned_huggingface_model_name="microsoft-beit-base-patch16-224-pt22k-ft22k-fridge-objects-multiclass-classification" @@ -120,12 +120,15 @@ fi # 3. Check if the model exists in the registry # need to confirm model show command works for registries outside the tenant (aka system registry) -if ! az ml model show --name $aml_registry_model_name --version $model_version --registry-name $registry_name +if ! az ml model show --name $aml_registry_model_name --label $model_label --registry-name $registry_name then - echo "Model $aml_registry_model_name:$model_version does not exist in registry $registry_name" + echo "Model $aml_registry_model_name:$model_label does not exist in registry $registry_name" exit 1 fi +# get the latest model version +model_version=$(az ml model show --name $model_name --label $model_label --registry-name $registry_name --query version --output tsv) + # 4. Prepare data python prepare_data.py # training data diff --git a/cli/foundation-models/system/finetune/image-classification/multiclass-classification/prepare_data.py b/cli/foundation-models/system/finetune/image-classification/multiclass-classification/prepare_data.py index 2cff013fe8..38770fb12c 100644 --- a/cli/foundation-models/system/finetune/image-classification/multiclass-classification/prepare_data.py +++ b/cli/foundation-models/system/finetune/image-classification/multiclass-classification/prepare_data.py @@ -40,9 +40,7 @@ def create_jsonl_and_mltable_files(uri_folder_data_path, dataset_dir): # We'll copy each JSONL file within its related MLTable folder training_mltable_path = os.path.join(dataset_parent_dir, "training-mltable-folder") - validation_mltable_path = os.path.join( - dataset_parent_dir, "validation-mltable-folder" - ) + validation_mltable_path = os.path.join(dataset_parent_dir, "validation-mltable-folder") # Create MLTable folders, if they don't exist os.makedirs(training_mltable_path, exist_ok=True) @@ -51,12 +49,8 @@ def create_jsonl_and_mltable_files(uri_folder_data_path, dataset_dir): train_validation_ratio = 5 # Path to the training and validation files - train_annotations_file = os.path.join( - training_mltable_path, "train_annotations.jsonl" - ) - validation_annotations_file = os.path.join( - validation_mltable_path, "validation_annotations.jsonl" - ) + train_annotations_file = os.path.join(training_mltable_path, "train_annotations.jsonl") + validation_annotations_file = os.path.join(validation_mltable_path, "validation_annotations.jsonl") # Baseline of json line dictionary json_line_sample = {"image_url": uri_folder_data_path, "label": ""} @@ -87,20 +81,15 @@ def create_jsonl_and_mltable_files(uri_folder_data_path, dataset_dir): print("done") # Create and save train mltable - train_mltable_file_contents = create_ml_table_file( - os.path.basename(train_annotations_file) - ) + train_mltable_file_contents = create_ml_table_file(os.path.basename(train_annotations_file)) save_ml_table_file(training_mltable_path, train_mltable_file_contents) # Create and save validation mltable - validation_mltable_file_contents = create_ml_table_file( - os.path.basename(validation_annotations_file) - ) + validation_mltable_file_contents = create_ml_table_file(os.path.basename(validation_annotations_file)) save_ml_table_file(validation_mltable_path, validation_mltable_file_contents) def upload_data_and_create_jsonl_mltable_files(ml_client, dataset_parent_dir): - # Create directory, if it does not exist os.makedirs(dataset_parent_dir, exist_ok=True) @@ -142,9 +131,7 @@ def upload_data_and_create_jsonl_mltable_files(ml_client, dataset_parent_dir): print("") print("Path to folder in Blob Storage:") print(uri_folder_data_asset.path) - create_jsonl_and_mltable_files( - uri_folder_data_path=uri_folder_data_asset.path, dataset_dir=dataset_dir - ) + create_jsonl_and_mltable_files(uri_folder_data_path=uri_folder_data_asset.path, dataset_dir=dataset_dir) def read_image(image_path): @@ -153,16 +140,12 @@ def read_image(image_path): if __name__ == "__main__": - parser = argparse.ArgumentParser( - description="Prepare data for image classification" - ) + parser = argparse.ArgumentParser(description="Prepare data for image classification") parser.add_argument("--subscription", type=str, help="Subscription ID") parser.add_argument("--resource_group", type=str, help="Resource group name") parser.add_argument("--workspace", type=str, help="Workspace name") - parser.add_argument( - "--data_path", type=str, default="./data", help="Dataset location" - ) + parser.add_argument("--data_path", type=str, default="./data", help="Dataset location") args, unknown = parser.parse_known_args() args_dict = vars(args) @@ -178,9 +161,7 @@ def read_image(image_path): workspace = args.workspace ml_client = MLClient(credential, subscription_id, resource_group, workspace) - upload_data_and_create_jsonl_mltable_files( - ml_client=ml_client, dataset_parent_dir=args.data_path - ) + upload_data_and_create_jsonl_mltable_files(ml_client=ml_client, dataset_parent_dir=args.data_path) sample_image = os.path.join(args.data_path, "fridgeObjects", "milk_bottle", "99.jpg") huggingface_request_json = { diff --git a/cli/foundation-models/system/finetune/image-classification/multilabel-classification/hftransformers-fridgeobjects-multilabel-classification.sh b/cli/foundation-models/system/finetune/image-classification/multilabel-classification/hftransformers-fridgeobjects-multilabel-classification.sh index 8078872a60..e543f89fa5 100644 --- a/cli/foundation-models/system/finetune/image-classification/multilabel-classification/hftransformers-fridgeobjects-multilabel-classification.sh +++ b/cli/foundation-models/system/finetune/image-classification/multilabel-classification/hftransformers-fridgeobjects-multilabel-classification.sh @@ -22,7 +22,7 @@ huggingface_model_name="microsoft/beit-base-patch16-224-pt22k-ft22k" # This is the foundation model for finetuning from azureml system registry # using the latest version of the model - not working yet aml_registry_model_name="microsoft-beit-base-patch16-224-pt22k-ft22k" -model_version=1 +model_label="latest" version=$(date +%s) finetuned_huggingface_model_name="microsoft-beit-base-patch16-224-pt22k-ft22k-fridge-objects-multilabel-classification" @@ -119,11 +119,15 @@ fi # 3. Check if the model exists in the registry # need to confirm model show command works for registries outside the tenant (aka system registry) -if ! az ml model show --name $aml_registry_model_name --version $model_version --registry-name $registry_name +if ! az ml model show --name $aml_registry_model_name --label $model_label --registry-name $registry_name then - echo "Model $aml_registry_model_name:$model_version does not exist in registry $registry_name" + echo "Model $aml_registry_model_name:$model_label does not exist in registry $registry_name" exit 1 fi + +# get the latest model version +model_version=$(az ml model show --name $model_name --label $model_label --registry-name $registry_name --query version --output tsv) + # 4. Prepare data python prepare_data.py # training data diff --git a/cli/foundation-models/system/finetune/image-classification/multilabel-classification/prepare_data.py b/cli/foundation-models/system/finetune/image-classification/multilabel-classification/prepare_data.py index a93169242c..99c9878c7c 100644 --- a/cli/foundation-models/system/finetune/image-classification/multilabel-classification/prepare_data.py +++ b/cli/foundation-models/system/finetune/image-classification/multilabel-classification/prepare_data.py @@ -40,9 +40,7 @@ def create_jsonl_and_mltable_files(uri_folder_data_path, dataset_dir): # We'll copy each JSONL file within its related MLTable folder training_mltable_path = os.path.join(dataset_parent_dir, "training-mltable-folder") - validation_mltable_path = os.path.join( - dataset_parent_dir, "validation-mltable-folder" - ) + validation_mltable_path = os.path.join(dataset_parent_dir, "validation-mltable-folder") # Create MLTable folders, if they don't exist os.makedirs(training_mltable_path, exist_ok=True) @@ -51,12 +49,8 @@ def create_jsonl_and_mltable_files(uri_folder_data_path, dataset_dir): train_validation_ratio = 5 # Path to the training and validation files - train_annotations_file = os.path.join( - training_mltable_path, "train_annotations.jsonl" - ) - validation_annotations_file = os.path.join( - validation_mltable_path, "validation_annotations.jsonl" - ) + train_annotations_file = os.path.join(training_mltable_path, "train_annotations.jsonl") + validation_annotations_file = os.path.join(validation_mltable_path, "validation_annotations.jsonl") # Path to the labels file. label_file = os.path.join(dataset_dir, "labels.csv") @@ -90,26 +84,23 @@ def create_jsonl_and_mltable_files(uri_folder_data_path, dataset_dir): print("done") # Create and save train mltable - train_mltable_file_contents = create_ml_table_file( - os.path.basename(train_annotations_file) - ) + train_mltable_file_contents = create_ml_table_file(os.path.basename(train_annotations_file)) save_ml_table_file(training_mltable_path, train_mltable_file_contents) # Create and save validation mltable - validation_mltable_file_contents = create_ml_table_file( - os.path.basename(validation_annotations_file) - ) + validation_mltable_file_contents = create_ml_table_file(os.path.basename(validation_annotations_file)) save_ml_table_file(validation_mltable_path, validation_mltable_file_contents) def upload_data_and_create_jsonl_mltable_files(ml_client, dataset_parent_dir): - # Create directory, if it does not exist os.makedirs(dataset_parent_dir, exist_ok=True) # download data print("Downloading data.") - download_url = "https://cvbp-secondary.z19.web.core.windows.net/datasets/image_classification/multilabelFridgeObjects.zip" + download_url = ( + "https://cvbp-secondary.z19.web.core.windows.net/datasets/image_classification/multilabelFridgeObjects.zip" + ) # Extract current dataset name from dataset url dataset_name = os.path.basename(download_url).split(".")[0] @@ -145,9 +136,7 @@ def upload_data_and_create_jsonl_mltable_files(ml_client, dataset_parent_dir): print("") print("Path to folder in Blob Storage:") print(uri_folder_data_asset.path) - create_jsonl_and_mltable_files( - uri_folder_data_path=uri_folder_data_asset.path, dataset_dir=dataset_dir - ) + create_jsonl_and_mltable_files(uri_folder_data_path=uri_folder_data_asset.path, dataset_dir=dataset_dir) def read_image(image_path): @@ -156,16 +145,12 @@ def read_image(image_path): if __name__ == "__main__": - parser = argparse.ArgumentParser( - description="Prepare data for image classification" - ) + parser = argparse.ArgumentParser(description="Prepare data for image classification") parser.add_argument("--subscription", type=str, help="Subscription ID") parser.add_argument("--group", type=str, help="Resource group name") parser.add_argument("--workspace", type=str, help="Workspace name") - parser.add_argument( - "--data_path", type=str, default="./data", help="Dataset location" - ) + parser.add_argument("--data_path", type=str, default="./data", help="Dataset location") args, unknown = parser.parse_known_args() args_dict = vars(args) @@ -181,9 +166,7 @@ def read_image(image_path): workspace = args.workspace ml_client = MLClient(credential, subscription_id, resource_group, workspace) - upload_data_and_create_jsonl_mltable_files( - ml_client=ml_client, dataset_parent_dir=args.data_path - ) + upload_data_and_create_jsonl_mltable_files(ml_client=ml_client, dataset_parent_dir=args.data_path) sample_image = os.path.join(args.data_path, "multilabelFridgeObjects", "images", "56.jpg") huggingface_request_json = { diff --git a/cli/foundation-models/system/inference/image-classification/deploy-batch.yaml b/cli/foundation-models/system/inference/image-classification/deploy-batch.yaml new file mode 100644 index 0000000000..6428d4a2c4 --- /dev/null +++ b/cli/foundation-models/system/inference/image-classification/deploy-batch.yaml @@ -0,0 +1,9 @@ +$schema: https://azuremlschemas.azureedge.net/latest/batchDeployment.schema.json +name: demo +description: "Batch endpoint for for image-classification task" +type: model +resources: + instance_count: 1 +settings: + mini_batch_size: 1 + diff --git a/cli/foundation-models/system/inference/image-classification/deploy.yaml b/cli/foundation-models/system/inference/image-classification/deploy-online.yaml similarity index 58% rename from cli/foundation-models/system/inference/image-classification/deploy.yaml rename to cli/foundation-models/system/inference/image-classification/deploy-online.yaml index 336e5519f5..055f238f42 100644 --- a/cli/foundation-models/system/inference/image-classification/deploy.yaml +++ b/cli/foundation-models/system/inference/image-classification/deploy-online.yaml @@ -2,5 +2,11 @@ $schema: https://azuremlschemas.azureedge.net/latest/managedOnlineDeployment.sch name: demo instance_type: Standard_DS3_v2 instance_count: 1 +liveness_probe: + initial_delay: 180 + period: 180 + failure_threshold: 49 + timeout: 299 request_settings: - request_timeout_ms: 60000 \ No newline at end of file + request_timeout_ms: 60000 + diff --git a/cli/foundation-models/system/inference/image-classification/image-classification-batch-endpoint.sh b/cli/foundation-models/system/inference/image-classification/image-classification-batch-endpoint.sh new file mode 100644 index 0000000000..2f669741a0 --- /dev/null +++ b/cli/foundation-models/system/inference/image-classification/image-classification-batch-endpoint.sh @@ -0,0 +1,145 @@ + + +set -x +# the commands in this file map to steps in this notebook: https://aka.ms/azureml-infer-batch-sdk-image-classification +# the sample scoring file available in the same folder as the above notebook + +# script inputs +registry_name="azureml-preview" +subscription_id="" +resource_group_name="" +workspace_name="" + +# This is the model from system registry that needs to be deployed +model_name="microsoft-beit-base-patch16-224-pt22k-ft22k" + +model_label="latest" + +deployment_compute="cpu-cluster" +# todo: fetch deployment_sku from the min_inference_sku tag of the model +deployment_sku="Standard_DS3_v2" + + +version=$(date +%s) +endpoint_name="image-classification-$version" +deployment_name="demo-$version" + +# Prepare data for deployment +multi_label=0 +data_path="data_batch" +python ./prepare_data.py --is_multilabel $multi_label --mode "batch" --data_path $data_path +# sample request data in csv format with image column +if [ $multi_label -eq 1 ] +then + sample_request_csv="./data_batch/image_classification_multilabel_lis.csv" + sample_request_folder="./data_batch/multilabelFridgeObjects" +else + sample_request_csv="./data_batch/image_classification_multiclass_list.csv" + sample_request_folder="./data_batch/fridgeObjects" +fi + +# 1. Setup pre-requisites +if [ "$subscription_id" = "" ] || \ + ["$resource_group_name" = "" ] || \ + [ "$workspace_name" = "" ]; then + echo "Please update the script with the subscription_id, resource_group_name and workspace_name" + exit 1 +fi + +az account set -s $subscription_id +workspace_info="--resource-group $resource_group_name --workspace-name $workspace_name" + +# 2. Check if the model exists in the registry +# need to confirm model show command works for registries outside the tenant (aka system registry) +if ! az ml model show --name $model_name --label $model_label --registry-name $registry_name +then + echo "Model $model_name:$model_label does not exist in registry $registry_name" + exit 1 +fi + +# get the latest model version +model_version=$(az ml model show --name $model_name --label $model_label --registry-name $registry_name --query version --output tsv) + +# 3. check if compute $deployment_compute exists, else create it +if az ml compute show --name $deployment_compute $workspace_info +then + echo "Compute cluster $deployment_compute already exists" +else + echo "Creating compute cluster $deployment_compute" + az ml compute create --name $deployment_compute --type amlcompute --min-instances 0 --max-instances 2 --size $deployment_sku $workspace_info || { + echo "Failed to create compute cluster $deployment_compute" + exit 1 + } +fi + +# 4. Deploy the model to an endpoint +# create online endpoint +az ml batch-endpoint create --name $endpoint_name $workspace_info || { + echo "endpoint create failed"; exit 1; +} + +# deploy model from registry to endpoint in workspace +az ml batch-deployment create --file ./deploy-batch.yaml $workspace_info --set \ + endpoint_name=$endpoint_name model=azureml://registries/$registry_name/models/$model_name/versions/$model_version \ + compute=$deployment_compute \ + name=$deployment_name || { + echo "deployment create failed"; exit 1; +} + +# 5.2 Try a scoring request with image folder + +# Check if scoring folder exists +if [ -d $data_path ]; then + echo "Invoking endpoint $endpoint_name with following input:\n\n" + ls $data_path + echo "\n\n" +else + echo "Scoring folder $data_path does not exist" + exit 1 +fi + +# invoke the endpoint +folder_inference_job=$(az ml batch-endpoint invoke --name $endpoint_name \ + --deployment-name $deployment_name --input $sample_request_folder --input-type \ + uri_folder $workspace_info --query name --output tsv) || { + echo "endpoint invoke failed"; exit 1; +} + +# wait for the job to complete +az ml job stream --name $folder_inference_job $workspace_info || { + echo "job stream failed"; exit 1; +} + +# 5.2 Try a scoring request with csv file +# Note: If job failed with error Assertion Error (The actual length exceeded max length 100 MB) then +# please try with less number of input images or use ImageFolder Input mode. + +# Check if scoring data file exists +if [ -f $sample_request_csv ]; then + echo "Invoking endpoint $endpoint_name with following input:\n\n" + echo "\n\n" +else + echo "Scoring file $sample_request_csv does not exist" + exit 1 +fi + +# invoke the endpoint +csv_inference_job=$(az ml batch-endpoint invoke --name $endpoint_name \ + --deployment-name $deployment_name --input $sample_request_csv --input-type \ + uri_file $workspace_info --query name --output tsv) || { + echo "endpoint invoke failed"; exit 1; +} + +# wait for the job to complete +az ml job stream --name $csv_inference_job $workspace_info || { + echo "job stream failed"; exit 1; +} + +# 6. Delete the endpoint +# Batch endpoints use compute resources only when jobs are submitted. You can keep the +# batch endpoint for your reference without worrying about compute bills, or choose to delete the endpoint. +# If you created your compute cluster to have zero minimum instances and scale down soon after being idle, +# you won't be charged for an unused compute. +az ml batch-endpoint delete --name $endpoint_name $workspace_info --yes || { + echo "endpoint delete failed"; exit 1; +} diff --git a/cli/foundation-models/system/inference/image-classification/image-classification-online-endpoint.sh b/cli/foundation-models/system/inference/image-classification/image-classification-online-endpoint.sh index da32880ce7..b975fc96f4 100644 --- a/cli/foundation-models/system/inference/image-classification/image-classification-online-endpoint.sh +++ b/cli/foundation-models/system/inference/image-classification/image-classification-online-endpoint.sh @@ -11,18 +11,23 @@ workspace_name="" # This is the model from system registry that needs to be deployed model_name="microsoft-beit-base-patch16-224-pt22k-ft22k" # using the latest version of the model - not working yet -model_version=1 +model_version=2 version=$(date +%s) endpoint_name="image-classification-$version" # todo: fetch deployment_sku from the min_inference_sku tag of the model -deployment_sku="Standard_DS2_v3" +deployment_sku="Standard_DS3_v2" # Prepare data for deployment -python ./prepare_data.py --is_multilabel 0 +python ./prepare_data.py --is_multilabel 0 --data_path "data_online" --mode "online" # sample_request_data -sample_request_data="./sample_request_data.json" +if [ $multi_label -eq 1 ] +then + sample_request_data="./data_online/multilabelFridgeObjects/sample_request_data.json" +else + sample_request_data="./data_online/fridgeObjects/sample_request_data.json" +fi # 1. Setup pre-requisites if [ "$subscription_id" = "" ] || \ @@ -50,7 +55,7 @@ az ml online-endpoint create --name $endpoint_name $workspace_info || { } # deploy model from registry to endpoint in workspace -az ml online-deployment create --file deploy.yml $workspace_info --all-traffic --set \ +az ml online-deployment create --file deploy-online.yaml $workspace_info --all-traffic --set \ endpoint_name=$endpoint_name model=azureml://registries/$registry_name/models/$model_name/versions/$model_version \ instance_type=$deployment_sku || { echo "deployment create failed"; exit 1; @@ -77,4 +82,4 @@ az ml online-endpoint delete --name $endpoint_name $workspace_info --yes || { echo "endpoint delete failed"; exit 1; } -rm $sample_request_data \ No newline at end of file +rm $sample_request_data diff --git a/cli/foundation-models/system/inference/image-classification/prepare_data.py b/cli/foundation-models/system/inference/image-classification/prepare_data.py index db4893e78b..bfadf7fe42 100644 --- a/cli/foundation-models/system/inference/image-classification/prepare_data.py +++ b/cli/foundation-models/system/inference/image-classification/prepare_data.py @@ -2,24 +2,41 @@ import base64 import json import os -import urllib +import shutil +import urllib.request +import pandas as pd from zipfile import ZipFile -def download_and_unzip(dataset_parent_dir: str, is_multilabel_dataset: int): +def download_and_unzip(dataset_parent_dir: str, is_multilabel_dataset: int) -> None: + """Download image dataset and unzip it. + :param dataset_parent_dir: dataset parent directory to which dataset will be downloaded + :type dataset_parent_dir: str + :param is_multilabel_dataset: flag to indicate if dataset is multi-label or not + :type is_multilabel_dataset: int + """ # Create directory, if it does not exist os.makedirs(dataset_parent_dir, exist_ok=True) # download data if is_multilabel_dataset == 0: - download_url = "https://cvbp-secondary.z19.web.core.windows.net/datasets/image_classification/fridgeObjects.zip" + download_url = ( + "https://cvbp-secondary.z19.web.core.windows.net/datasets/image_classification/fridgeObjects.zip" + ) else: - download_url = "https://cvbp-secondary.z19.web.core.windows.net/datasets/image_classification/multilabelFridgeObjects.zip" + download_url = ( + "https://cvbp-secondary.z19.web.core.windows.net/datasets/image_classification/multilabelFridgeObjects.zip" + ) print(f"Downloading data from {download_url}") # Extract current dataset name from dataset url dataset_name = os.path.basename(download_url).split(".")[0] + # Get dataset path for later use + dataset_dir = os.path.join(dataset_parent_dir, dataset_name) + + if os.path.exists(dataset_dir): + shutil.rmtree(dataset_dir) # Get the name of zip file data_file = os.path.join(dataset_parent_dir, f"{dataset_name}.zip") @@ -34,43 +51,93 @@ def download_and_unzip(dataset_parent_dir: str, is_multilabel_dataset: int): print("done") # delete zip file os.remove(data_file) + return dataset_dir + +def read_image(image_path: str) -> bytes: + """Read image from path. -def read_image(image_path): + :param image_path: image path + :type image_path: str + :return: image in bytes format + :rtype: bytes + """ with open(image_path, "rb") as f: return f.read() -if __name__ == "__main__": - parser = argparse.ArgumentParser( - description="Prepare data for image classification" - ) - parser.add_argument( - "--data_path", type=str, default="./data", help="Dataset location" - ) - parser.add_argument( - "--is_multilabel", type=int, default=0, help="Is multilabel dataset" - ) - args, unknown = parser.parse_known_args() - args_dict = vars(args) - - download_and_unzip( - dataset_parent_dir=args.data_path, - is_multilabel_dataset=args.is_multilabel, - ) +def prepare_data_for_online_inference(dataset_dir: str, is_multilabel: int = 0) -> None: + """Prepare request json for online inference. - if args.is_multilabel == 0: - sample_image = os.path.join(args.data_path, "fridgeObjects", "milk_bottle", "99.jpg") + :param dataset_dir: dataset directory + :type dataset_dir: str + :param is_multilabel: flag to indicate if dataset is multi-label or not + :type is_multilabel: int + """ + if is_multilabel == 0: + sample_image = os.path.join(dataset_dir, "milk_bottle", "99.jpg") else: - sample_image = os.path.join(args.data_path, "multilabelFridgeObjects", "images", "56.jpg") + sample_image = os.path.join(dataset_dir, "images", "56.jpg") request_json = { - "inputs": { - "image": [base64.encodebytes(read_image(sample_image)).decode("utf-8")], + "input_data": { + "columns": ["image"], + "index": [0], + "data": [base64.encodebytes(read_image(sample_image)).decode("utf-8")], } } - request_file_name = "sample_request_data.json" + request_file_name = os.path.join(dataset_dir, "sample_request_data.json") with open(request_file_name, "w") as request_file: - json.dump(request_json, request_file) \ No newline at end of file + json.dump(request_json, request_file) + + +def prepare_data_for_batch_inference(dataset_dir: str, is_multilabel: int = 0) -> None: + """Prepare image folder and csv file for batch inference. + + This function will move all images to a single image folder and also create a csv + file with images in base64 format. + :param dataset_dir: dataset directory + :type dataset_dir: str + :param is_multilabel: flag to indicate if dataset is multi-label or not + :type is_multilabel: int + """ + image_list = [] + + csv_file_name = "image_classification_multilabel_lis.csv" if is_multilabel == 1 else \ + "image_classification_multiclass_list.csv" + + for dir_name in os.listdir(dataset_dir): + dir_path = os.path.join(dataset_dir, dir_name) + for path, _, files in os.walk(dir_path): + for file in files: + image = read_image(os.path.join(path, file)) + image_list.append(base64.encodebytes(image).decode("utf-8")) + shutil.move(os.path.join(path, file), dataset_dir) + if os.path.isdir(dir_path): + shutil.rmtree(dir_path) + else: + os.remove(dir_path) + df = pd.DataFrame(image_list, columns=["image"]).sample(10) + df.to_csv(os.path.join(os.path.dirname(dataset_dir), csv_file_name), index=False, header=True) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Prepare data for image classification") + parser.add_argument("--data_path", type=str, default="data", help="Dataset location") + parser.add_argument("--is_multilabel", type=int, default=0, help="Is multilabel dataset") + parser.add_argument("--mode", type=str, default="online", help="prepare data for online or batch inference") + + args, unknown = parser.parse_known_args() + args_dict = vars(args) + + dataset_dir = download_and_unzip( + dataset_parent_dir=os.path.join(os.path.dirname(os.path.abspath(__file__)), args.data_path), + is_multilabel_dataset=args.is_multilabel, + ) + + if args.mode == "online": + prepare_data_for_online_inference(dataset_dir=dataset_dir, is_multilabel=args.is_multilabel) + else: + prepare_data_for_batch_inference(dataset_dir=dataset_dir, is_multilabel=args.is_multilabel) diff --git a/sdk/python/foundation-models/system/finetune/image-classification/multiclass-classification/hftransformers-fridgeobjects-multiclass-classification.ipynb b/sdk/python/foundation-models/system/finetune/image-classification/multiclass-classification/hftransformers-fridgeobjects-multiclass-classification.ipynb index c581ce498d..13f9885eab 100644 --- a/sdk/python/foundation-models/system/finetune/image-classification/multiclass-classification/hftransformers-fridgeobjects-multiclass-classification.ipynb +++ b/sdk/python/foundation-models/system/finetune/image-classification/multiclass-classification/hftransformers-fridgeobjects-multiclass-classification.ipynb @@ -80,9 +80,7 @@ "from azure.identity import DefaultAzureCredential\n", "\n", "\n", - "experiment_name = (\n", - " \"AzureML-Train-Finetune-Vision-MultiClass-Samples\" # can rename to any valid name\n", - ")\n", + "experiment_name = \"AzureML-Train-Finetune-Vision-MultiClass-Samples\" # can rename to any valid name\n", "\n", "credential = DefaultAzureCredential()\n", "workspace_ml_client = None\n", @@ -98,9 +96,7 @@ " resource_group = \"RESOURCE_GROUP\"\n", " workspace_name = \"AML_WORKSPACE_NAME\"\n", "\n", - "workspace_ml_client = MLClient(\n", - " credential, subscription_id, resource_group, workspace_name\n", - ")\n", + "workspace_ml_client = MLClient(credential, subscription_id, resource_group, workspace_name)\n", "registry_ml_client = MLClient(\n", " credential,\n", " subscription_id,\n", @@ -218,8 +214,8 @@ "huggingface_model_name = \"microsoft/beit-base-patch16-224-pt22k-ft22k\"\n", "\n", "aml_registry_model_name = \"microsoft-beit-base-patch16-224-pt22k-ft22k\"\n", - "model_version = \"1\"\n", - "foundation_model = registry_ml_client.models.get(aml_registry_model_name, model_version)\n", + "foundation_models = registry_ml_client.models.list(aml_registry_model_name)\n", + "foundation_model = max(foundation_models, key=lambda x: x.version)\n", "print(f\"\\n\\nUsing model name: {foundation_model.name}, version: {foundation_model.version}, id: {foundation_model.id} for fine tuning\")" ] }, @@ -363,9 +359,7 @@ "\n", "# Path to the training and validation files\n", "train_annotations_file = os.path.join(training_mltable_path, \"train_annotations.jsonl\")\n", - "validation_annotations_file = os.path.join(\n", - " validation_mltable_path, \"validation_annotations.jsonl\"\n", - ")\n", + "validation_annotations_file = os.path.join(validation_mltable_path, \"validation_annotations.jsonl\")\n", "\n", "# Baseline of json line dictionary\n", "json_line_sample = {\n", @@ -441,15 +435,11 @@ "\n", "\n", "# Create and save train mltable\n", - "train_mltable_file_contents = create_ml_table_file(\n", - " os.path.basename(train_annotations_file)\n", - ")\n", + "train_mltable_file_contents = create_ml_table_file(os.path.basename(train_annotations_file))\n", "save_ml_table_file(training_mltable_path, train_mltable_file_contents)\n", "\n", "# Create and save validation mltable\n", - "validation_mltable_file_contents = create_ml_table_file(\n", - " os.path.basename(validation_annotations_file)\n", - ")\n", + "validation_mltable_file_contents = create_ml_table_file(os.path.basename(validation_annotations_file))\n", "save_ml_table_file(validation_mltable_path, validation_mltable_file_contents)" ] }, @@ -550,13 +540,9 @@ "\n", "# Ensure that the user provides only one of mlflow_model or model_name\n", "if pipeline_component_args.get(\"mlflow_model\") is None and pipeline_component_args.get(\"model_name\") is None:\n", - " raise ValueError(\n", - " \"You must specify either mlflow_model or model_name for the model to finetune\"\n", - " )\n", + " raise ValueError(\"You must specify either mlflow_model or model_name for the model to finetune\")\n", "if pipeline_component_args.get(\"mlflow_model\") is not None and pipeline_component_args.get(\"model_name\") is not None:\n", - " raise ValueError(\n", - " \"You must specify ONLY one of mlflow_model and model_name for the model to finetune\"\n", - " )\n", + " raise ValueError(\"You must specify ONLY one of mlflow_model and model_name for the model to finetune\")\n", "elif pipeline_component_args.get(\"mlflow_model\") is None and pipeline_component_args.get(\"model_name\") is not None:\n", " use_model_name = huggingface_model_name\n", "elif pipeline_component_args.get(\"mlflow_model\") is not None and pipeline_component_args.get(\"model_name\") is None:\n", @@ -620,9 +606,7 @@ "source": [ "transformers_pipeline_object = create_pipeline_transformers()\n", "\n", - "transformers_pipeline_object.display_name = (\n", - " use_model_name + \"_transformers_pipeline_component_run_\" + \"multiclass\"\n", - ")\n", + "transformers_pipeline_object.display_name = use_model_name + \"_transformers_pipeline_component_run_\" + \"multiclass\"\n", "# Don't use cached results from previous jobs\n", "transformers_pipeline_object.settings.force_rerun = True\n", "\n", @@ -679,9 +663,7 @@ "import mlflow\n", "\n", "# Obtain the tracking URL from MLClient\n", - "MLFLOW_TRACKING_URI = workspace_ml_client.workspaces.get(\n", - " name=workspace_ml_client.workspace_name\n", - ").mlflow_tracking_uri\n", + "MLFLOW_TRACKING_URI = workspace_ml_client.workspaces.get(name=workspace_ml_client.workspace_name).mlflow_tracking_uri\n", "\n", "print(MLFLOW_TRACKING_URI)" ] @@ -725,15 +707,15 @@ "source": [ "# concat 'tags.mlflow.rootRunId=' and pipeline_job.name in single quotes as filter variable\n", "filter = \"tags.mlflow.rootRunId='\" + transformers_pipeline_run.name + \"'\"\n", - "runs = mlflow.search_runs(experiment_names=[experiment_name], filter_string = filter, output_format=\"list\")\n", - "# get the training and evaluation runs. \n", + "runs = mlflow.search_runs(experiment_names=[experiment_name], filter_string=filter, output_format=\"list\")\n", + "# get the training and evaluation runs.\n", "# using a hacky way till 'Bug 2320997: not able to show eval metrics in FT notebooks - mlflow client now showing display names' is fixed\n", "for run in runs:\n", " # check if run.data.metrics.epoch exists\n", - " if 'epoch' in run.data.metrics:\n", + " if \"epoch\" in run.data.metrics:\n", " training_run = run\n", " # else, check if run.data.metrics.accuracy exists\n", - " elif 'accuracy' in run.data.metrics:\n", + " elif \"accuracy\" in run.data.metrics:\n", " evaluation_run = run" ] }, @@ -775,8 +757,9 @@ "outputs": [], "source": [ "import time\n", + "\n", "# genrating a unique timestamp that can be used for names and versions that need to be unique\n", - "timestamp = str(int(time.time())) " + "timestamp = str(int(time.time()))" ] }, { @@ -796,19 +779,21 @@ "print(f\"Path to register model: {model_path_from_job}\")\n", "\n", "finetuned_model_name = f\"{use_model_name.replace('/', '-')}-fridge-objects-multiclass-classification\"\n", - "finetuned_model_description = f\"{use_model_name.replace('/', '-')} fine tuned model for fridge objects multiclass classification\"\n", + "finetuned_model_description = (\n", + " f\"{use_model_name.replace('/', '-')} fine tuned model for fridge objects multiclass classification\"\n", + ")\n", "prepare_to_register_model = Model(\n", " path=model_path_from_job,\n", " type=AssetTypes.MLFLOW_MODEL,\n", " name=finetuned_model_name,\n", - " version=timestamp, # use timestamp as version to avoid version conflict\n", - " description=finetuned_model_description\n", + " version=timestamp, # use timestamp as version to avoid version conflict\n", + " description=finetuned_model_description,\n", ")\n", "print(f\"Prepare to register model: \\n{prepare_to_register_model}\")\n", "\n", - "# Register the model from pipeline job output \n", + "# Register the model from pipeline job output\n", "registered_model = workspace_ml_client.models.create_or_update(prepare_to_register_model)\n", - "print(f\"Registered model: {registered_model}\")\n" + "print(f\"Registered model: {registered_model}\")" ] }, { @@ -830,10 +815,10 @@ "from azure.ai.ml.entities import ManagedOnlineEndpoint, ManagedOnlineDeployment\n", "\n", "# Endpoint names need to be unique in a region, hence using timestamp to create unique endpoint name\n", - "online_endpoint_name = \"hf-mc-fridge-items-\" + datetime.datetime.now().strftime(\n", - " \"%m%d%H%M\"\n", + "online_endpoint_name = \"hf-mc-fridge-items-\" + datetime.datetime.now().strftime(\"%m%d%H%M\")\n", + "online_endpoint_description = (\n", + " f\"Online endpoint for {registered_model.name}, finetuned for fridge objects multiclass classification\"\n", ")\n", - "online_endpoint_description = f\"Online endpoint for {registered_model.name}, finetuned for fridge objects multiclass classification\"\n", "# Create an online endpoint\n", "endpoint = ManagedOnlineEndpoint(\n", " name=online_endpoint_name,\n", @@ -863,12 +848,10 @@ " endpoint_name=online_endpoint_name,\n", " model=registered_model.id,\n", " # use GPU instance type like Standard_NC6s_v3 for faster explanations\n", - " instance_type=\"Standard_DS3_V2\", #\"Standard_DS3_V2\",\n", + " instance_type=\"Standard_DS3_V2\", # \"Standard_DS3_V2\",\n", " instance_count=1,\n", " request_settings=OnlineRequestSettings(\n", - " max_concurrent_requests_per_instance=1,\n", - " request_timeout_ms=5000, #90000,\n", - " max_queue_wait_ms=500\n", + " max_concurrent_requests_per_instance=1, request_timeout_ms=5000, max_queue_wait_ms=500 # 90000,\n", " ),\n", " liveness_probe=ProbeSettings(\n", " failure_threshold=30,\n", @@ -933,10 +916,12 @@ "\n", "sample_image = os.path.join(dataset_dir, \"milk_bottle\", \"99.jpg\")\n", "\n", + "\n", "def read_image(image_path):\n", " with open(image_path, \"rb\") as f:\n", " return f.read()\n", "\n", + "\n", "request_json = {\n", " \"inputs\": {\n", " \"image\": [base64.encodebytes(read_image(sample_image)).decode(\"utf-8\")],\n", diff --git a/sdk/python/foundation-models/system/finetune/image-classification/multilabel-classification/hftransformers-fridgeobjects-multilabel-classification.ipynb b/sdk/python/foundation-models/system/finetune/image-classification/multilabel-classification/hftransformers-fridgeobjects-multilabel-classification.ipynb index 88bde6c4ed..b045761aab 100644 --- a/sdk/python/foundation-models/system/finetune/image-classification/multilabel-classification/hftransformers-fridgeobjects-multilabel-classification.ipynb +++ b/sdk/python/foundation-models/system/finetune/image-classification/multilabel-classification/hftransformers-fridgeobjects-multilabel-classification.ipynb @@ -80,9 +80,7 @@ "from azure.identity import DefaultAzureCredential\n", "\n", "\n", - "experiment_name = (\n", - " \"AzureML-Train-Finetune-Vision-MultiLabel-Samples\" # can rename to any valid name\n", - ")\n", + "experiment_name = \"AzureML-Train-Finetune-Vision-MultiLabel-Samples\" # can rename to any valid name\n", "\n", "credential = DefaultAzureCredential()\n", "workspace_ml_client = None\n", @@ -97,9 +95,7 @@ " subscription_id = \"SUBSCRIPTION_ID\"\n", " resource_group = \"RESOURCE_GROUP\"\n", " workspace_name = \"AML_WORKSPACE_NAME\"\n", - " workspace_ml_client = MLClient(\n", - " credential, subscription_id, resource_group, workspace_name\n", - " )\n", + " workspace_ml_client = MLClient(credential, subscription_id, resource_group, workspace_name)\n", "\n", "registry_ml_client = MLClient(\n", " credential,\n", @@ -218,8 +214,8 @@ "huggingface_model_name = \"microsoft/beit-base-patch16-224-pt22k-ft22k\"\n", "\n", "aml_registry_model_name = \"microsoft-beit-base-patch16-224-pt22k-ft22k\"\n", - "model_version = \"1\"\n", - "foundation_model = registry_ml_client.models.get(aml_registry_model_name, model_version)\n", + "foundation_models = registry_ml_client.models.list(aml_registry_model_name)\n", + "foundation_model = max(foundation_models, key=lambda x: x.version)\n", "print(f\"\\n\\nUsing model name: {foundation_model.name}, version: {foundation_model.version}, id: {foundation_model.id} for fine tuning\")" ] }, @@ -256,7 +252,9 @@ "os.makedirs(dataset_parent_dir, exist_ok=True)\n", "\n", "# download data\n", - "download_url = \"https://cvbp-secondary.z19.web.core.windows.net/datasets/image_classification/multilabelFridgeObjects.zip\"\n", + "download_url = (\n", + " \"https://cvbp-secondary.z19.web.core.windows.net/datasets/image_classification/multilabelFridgeObjects.zip\"\n", + ")\n", "\n", "# Extract current dataset name from dataset url\n", "dataset_name = os.path.split(download_url)[-1].split(\".\")[0]\n", @@ -359,9 +357,7 @@ "\n", "# Path to the training and validation files\n", "train_annotations_file = os.path.join(training_mltable_path, \"train_annotations.jsonl\")\n", - "validation_annotations_file = os.path.join(\n", - " validation_mltable_path, \"validation_annotations.jsonl\"\n", - ")\n", + "validation_annotations_file = os.path.join(validation_mltable_path, \"validation_annotations.jsonl\")\n", "\n", "# Baseline of json line dictionary\n", "json_line_sample = {\n", @@ -439,15 +435,11 @@ "\n", "\n", "# Create and save train mltable\n", - "train_mltable_file_contents = create_ml_table_file(\n", - " os.path.basename(train_annotations_file)\n", - ")\n", + "train_mltable_file_contents = create_ml_table_file(os.path.basename(train_annotations_file))\n", "save_ml_table_file(training_mltable_path, train_mltable_file_contents)\n", "\n", "# Save train and validation mltable\n", - "validation_mltable_file_contents = create_ml_table_file(\n", - " os.path.basename(validation_annotations_file)\n", - ")\n", + "validation_mltable_file_contents = create_ml_table_file(os.path.basename(validation_annotations_file))\n", "save_ml_table_file(validation_mltable_path, validation_mltable_file_contents)" ] }, @@ -548,13 +540,9 @@ "\n", "# Ensure that the user provides only one of mlflow_model or model_name\n", "if pipeline_component_args.get(\"mlflow_model\") is None and pipeline_component_args.get(\"model_name\") is None:\n", - " raise ValueError(\n", - " \"You must specify either mlflow_model or model_name for the model to finetune\"\n", - " )\n", + " raise ValueError(\"You must specify either mlflow_model or model_name for the model to finetune\")\n", "if pipeline_component_args.get(\"mlflow_model\") is not None and pipeline_component_args.get(\"model_name\") is not None:\n", - " raise ValueError(\n", - " \"You must specify ONLY one of mlflow_model and model_name for the model to finetune\"\n", - " )\n", + " raise ValueError(\"You must specify ONLY one of mlflow_model and model_name for the model to finetune\")\n", "elif pipeline_component_args.get(\"mlflow_model\") is None and pipeline_component_args.get(\"model_name\") is not None:\n", " use_model_name = huggingface_model_name\n", "elif pipeline_component_args.get(\"mlflow_model\") is not None and pipeline_component_args.get(\"model_name\") is None:\n", @@ -618,9 +606,7 @@ "source": [ "transformers_pipeline_object = create_pipeline_transformers()\n", "\n", - "transformers_pipeline_object.display_name = (\n", - " use_model_name + \"_transformers_pipeline_component_run_\" + \"multilabel\"\n", - ")\n", + "transformers_pipeline_object.display_name = use_model_name + \"_transformers_pipeline_component_run_\" + \"multilabel\"\n", "# Don't use cached results from previous jobs\n", "transformers_pipeline_object.settings.force_rerun = True\n", "\n", @@ -677,9 +663,7 @@ "import mlflow\n", "\n", "# Obtain the tracking URL from MLClient\n", - "MLFLOW_TRACKING_URI = workspace_ml_client.workspaces.get(\n", - " name=workspace_ml_client.workspace_name\n", - ").mlflow_tracking_uri\n", + "MLFLOW_TRACKING_URI = workspace_ml_client.workspaces.get(name=workspace_ml_client.workspace_name).mlflow_tracking_uri\n", "\n", "print(MLFLOW_TRACKING_URI)" ] @@ -723,15 +707,15 @@ "source": [ "# concat 'tags.mlflow.rootRunId=' and pipeline_job.name in single quotes as filter variable\n", "filter = \"tags.mlflow.rootRunId='\" + transformers_pipeline_run.name + \"'\"\n", - "runs = mlflow.search_runs(experiment_names=[experiment_name], filter_string = filter, output_format=\"list\")\n", - "# get the training and evaluation runs. \n", + "runs = mlflow.search_runs(experiment_names=[experiment_name], filter_string=filter, output_format=\"list\")\n", + "# get the training and evaluation runs.\n", "# using a hacky way till 'Bug 2320997: not able to show eval metrics in FT notebooks - mlflow client now showing display names' is fixed\n", "for run in runs:\n", " # check if run.data.metrics.epoch exists\n", - " if 'epoch' in run.data.metrics:\n", + " if \"epoch\" in run.data.metrics:\n", " training_run = run\n", " # else, check if run.data.metrics.accuracy exists\n", - " elif 'accuracy' in run.data.metrics:\n", + " elif \"accuracy\" in run.data.metrics:\n", " evaluation_run = run" ] }, @@ -773,8 +757,9 @@ "outputs": [], "source": [ "import time\n", + "\n", "# genrating a unique timestamp that can be used for names and versions that need to be unique\n", - "timestamp = str(int(time.time())) " + "timestamp = str(int(time.time()))" ] }, { @@ -794,19 +779,21 @@ "print(f\"Path to register model: {model_path_from_job}\")\n", "\n", "finetuned_model_name = f\"{use_model_name.replace('/', '-')}-fridge-objects-multilabel-classification\"\n", - "finetuned_model_description = f\"{use_model_name.replace('/', '-')} fine tuned model for fridge objects multilabel classification\"\n", + "finetuned_model_description = (\n", + " f\"{use_model_name.replace('/', '-')} fine tuned model for fridge objects multilabel classification\"\n", + ")\n", "prepare_to_register_model = Model(\n", " path=model_path_from_job,\n", " type=AssetTypes.MLFLOW_MODEL,\n", " name=finetuned_model_name,\n", - " version=timestamp, # use timestamp as version to avoid version conflict\n", - " description=finetuned_model_description\n", + " version=timestamp, # use timestamp as version to avoid version conflict\n", + " description=finetuned_model_description,\n", ")\n", "print(f\"Prepare to register model: \\n{prepare_to_register_model}\")\n", "\n", - "# Register the model from pipeline job output \n", + "# Register the model from pipeline job output\n", "registered_model = workspace_ml_client.models.create_or_update(prepare_to_register_model)\n", - "print(f\"Registered model: {registered_model}\")\n" + "print(f\"Registered model: {registered_model}\")" ] }, { @@ -828,10 +815,10 @@ "from azure.ai.ml.entities import ManagedOnlineEndpoint, ManagedOnlineDeployment\n", "\n", "# Endpoint names need to be unique in a region, hence using timestamp to create unique endpoint name\n", - "online_endpoint_name = \"hf-ml-fridge-items-\" + datetime.datetime.now().strftime(\n", - " \"%m%d%H%M\"\n", + "online_endpoint_name = \"hf-ml-fridge-items-\" + datetime.datetime.now().strftime(\"%m%d%H%M\")\n", + "online_endpoint_description = (\n", + " f\"Online endpoint for {registered_model.name}, finetuned for fridge objects multilabel classification\"\n", ")\n", - "online_endpoint_description = f\"Online endpoint for {registered_model.name}, finetuned for fridge objects multilabel classification\"\n", "# Create an online endpoint\n", "endpoint = ManagedOnlineEndpoint(\n", " name=online_endpoint_name,\n", @@ -925,10 +912,12 @@ "\n", "sample_image = os.path.join(dataset_dir, \"images\", \"56.jpg\")\n", "\n", + "\n", "def read_image(image_path):\n", " with open(image_path, \"rb\") as f:\n", " return f.read()\n", "\n", + "\n", "request_json = {\n", " \"inputs\": {\n", " \"image\": [base64.encodebytes(read_image(sample_image)).decode(\"utf-8\")],\n", diff --git a/sdk/python/foundation-models/system/inference/image-classification/image-classification-batch-endpoint.ipynb b/sdk/python/foundation-models/system/inference/image-classification/image-classification-batch-endpoint.ipynb new file mode 100644 index 0000000000..068c1ece9d --- /dev/null +++ b/sdk/python/foundation-models/system/inference/image-classification/image-classification-batch-endpoint.ipynb @@ -0,0 +1,504 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Image Classification Inference using Batch Endpoints\n", + "\n", + "This sample shows how deploy `image-classification` type models to an batch endpoint for inference.\n", + "\n", + "### Task\n", + "`image-classification` tasks assign label(s) or class(es) to an image. There are two common types of `image-classification` tasks:\n", + "\n", + "* MultiClass: An image is categorised into one of the classes.\n", + "* MultiLabel: An image can be categorised into more than one class.\n", + " \n", + "### Model\n", + "Models that can perform the `image-classification` task are tagged with `image-classification`. We will use the `microsoft-beit-base-patch16-224-pt22k-ft22k` model in this notebook. If you opened this notebook from a specific model card, remember to replace the specific model name. If you don't find a model that suits your scenario or domain, you can discover and [import models from HuggingFace hub](../../import/import-model-from-huggingface.ipynb) and then use them for inference. \n", + "\n", + "### Inference data\n", + "We will use the [fridgeObjects](https://cvbp-secondary.z19.web.core.windows.net/datasets/image_classification/fridgeObjects.zip) dataset.\n", + "\n", + "\n", + "### Outline\n", + "* Setup pre-requisites.\n", + "* Pick a model to deploy.\n", + "* Prepare data for inference.\n", + " * Using ImageFolder\n", + " * Using CSV file\n", + "* Deploy the model for batch inference.\n", + "* Test the endpoint.\n", + "* Clean up resources." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 1. Setup pre-requisites\n", + "* Install dependencies\n", + "* Connect to AzureML Workspace. Learn more at [set up SDK authentication](https://learn.microsoft.com/en-us/azure/machine-learning/how-to-setup-authentication?tabs=sdk). Replace ``, `` and `` below.\n", + "* Connect to `azureml` system registry" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from azure.ai.ml import MLClient, Input\n", + "from azure.ai.ml.entities import AmlCompute\n", + "from azure.ai.ml.constants import AssetTypes\n", + "from azure.identity import DefaultAzureCredential, InteractiveBrowserCredential\n", + "import time\n", + "\n", + "try:\n", + " credential = DefaultAzureCredential()\n", + " credential.get_token(\"https://management.azure.com/.default\")\n", + "except Exception as ex:\n", + " credential = InteractiveBrowserCredential()\n", + "\n", + "try:\n", + " workspace_ml_client = MLClient.from_config(credential)\n", + " subscription_id = workspace_ml_client.subscription_id\n", + " resource_group = workspace_ml_client.resource_group_name\n", + " workspace_name = workspace_ml_client.workspace_name\n", + "except Exception as ex:\n", + " print(ex)\n", + " # Enter details of your AML workspace\n", + " subscription_id = \"\"\n", + " resource_group = \"\"\n", + " workspace_name = \"\"\n", + "\n", + "workspace_ml_client = MLClient(credential, subscription_id, resource_group, workspace_name)\n", + "\n", + "# the models, fine tuning pipelines and environments are available in the AzureML system registry, \"azureml-preview\"\n", + "registry_ml_client = MLClient(\n", + " credential,\n", + " subscription_id,\n", + " resource_group,\n", + " registry_name=\"azureml-preview\",\n", + ")\n", + "# generating a unique timestamp that can be used for names and versions that need to be unique\n", + "timestamp = str(int(time.time()))" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Create a compute cluster.\n", + "Use the model card from the AzureML system registry to check the minimum required inferencing SKU, referenced as size below. If you already have a sufficient compute cluster, you can simply define the name in compute_name in the following code block." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from azure.ai.ml.entities import AmlCompute\n", + "from azure.core.exceptions import ResourceNotFoundError\n", + "\n", + "compute_name = \"cpu-cluster\"\n", + "\n", + "try:\n", + " _ = workspace_ml_client.compute.get(compute_name)\n", + " print(\"Found existing compute target.\")\n", + "except ResourceNotFoundError:\n", + " print(\"Creating a new compute target...\")\n", + " compute_config = AmlCompute(\n", + " name=compute_name,\n", + " description=\"An AML compute cluster\",\n", + " size=\"Standard_DS3_V2\",\n", + " min_instances=0,\n", + " max_instances=3,\n", + " idle_time_before_scale_down=120,\n", + " )\n", + " workspace_ml_client.begin_create_or_update(compute_config).result()" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 2. Pick a model to deploy\n", + "\n", + "Browse models in the Model Catalog in the AzureML Studio, filtering by the `image-classification` task. In this example, we use the `microsoft-beit-base-patch16-224-pt22k-ft22k ` model. If you have opened this notebook for a different model, replace the model name and version accordingly. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "model_name = \"microsoft-beit-base-patch16-224-pt22k-ft22k\"\n", + "\n", + "foundation_models = registry_ml_client.models.list(name=model_name)\n", + "foundation_model = max(foundation_models, key=lambda x: x.version)\n", + "print(\n", + " f\"\\n\\nUsing model name: {foundation_model.name}, version: {foundation_model.version}, id: {foundation_model.id} for inferencing\"\n", + ")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 3. Prepare data for inference\n", + "\n", + "We will use the [fridgeObjects](https://cvbp-secondary.z19.web.core.windows.net/datasets/image_classification/fridgeObjects.zip) dataset. The fridge object dataset is stored in a directory. There are four different folders inside:\n", + "- /water_bottle\n", + "- /milk_bottle\n", + "- /carton\n", + "- /can\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import urllib\n", + "import shutil\n", + "from zipfile import ZipFile\n", + "\n", + "# Change to a different location if you prefer\n", + "dataset_parent_dir = \"./data\"\n", + "\n", + "# create data folder if it doesnt exist.\n", + "os.makedirs(dataset_parent_dir, exist_ok=True)\n", + "\n", + "# download data\n", + "download_url = \"https://cvbp-secondary.z19.web.core.windows.net/datasets/image_classification/fridgeObjects.zip\"\n", + "\n", + "# Extract current dataset name from dataset url\n", + "dataset_name = os.path.split(download_url)[-1].split(\".\")[0]\n", + "# Get dataset path for later use\n", + "dataset_dir = os.path.join(dataset_parent_dir, dataset_name)\n", + "\n", + "if os.path.exists(dataset_dir):\n", + " shutil.rmtree(dataset_dir)\n", + "\n", + "# Get the data zip file path\n", + "data_file = os.path.join(dataset_parent_dir, f\"{dataset_name}.zip\")\n", + "\n", + "# Download the dataset\n", + "urllib.request.urlretrieve(download_url, filename=data_file)\n", + "\n", + "# extract files\n", + "with ZipFile(data_file, \"r\") as zip:\n", + " print(\"extracting files...\")\n", + " zip.extractall(path=dataset_parent_dir)\n", + " print(\"done\")\n", + "\n", + "# delete zip file\n", + "os.remove(data_file)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### 3.1 Arrange images in common folder for batch inference input" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for dir_name in os.listdir(dataset_dir):\n", + " dir_path = os.path.join(dataset_dir, dir_name)\n", + " for path, subdirs, files in os.walk(dir_path):\n", + " for file in files:\n", + " shutil.move(os.path.join(path, file), dataset_dir)\n", + "\n", + " shutil.rmtree(dir_path)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### 3.2 Prepare CSV file with base64 images for batch inference input\n", + "\n", + "We can provide input images to batch inference either in a folder containing images or in a csv file containing \"image\" named column having images in base 64 format.\n", + "\n", + "Note: If job failed with error Assertion Error (The actual length exceeded max length 100 MB) then please try with less number of input images or use ImageFolder Input mode." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import base64\n", + "import pandas as pd\n", + "\n", + "image_list = []\n", + "csv_file_path = os.path.join(dataset_parent_dir, \"image_list.csv\")\n", + "\n", + "for image in os.listdir(dataset_dir):\n", + " with open(os.path.join(dataset_dir, image), \"rb\") as f:\n", + " data = f.read()\n", + " data = base64.encodebytes(data).decode(\"utf-8\")\n", + " image_list.append(data)\n", + "\n", + "df = pd.DataFrame(image_list, columns=[\"image\"]).sample(10)\n", + "df.to_csv(csv_file_path, index=False, header=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from IPython.display import Image\n", + "\n", + "sample_image = os.path.join(dataset_dir, \"99.jpg\")\n", + "Image(filename=sample_image)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 4. Deploy the model to an online endpoint\n", + "Batch endpoints are endpoints that are used to do batch inferencing on large volumes of data over a period of time. The endpoints receive pointers to data and run jobs asynchronously to process the data in parallel on compute clusters. Batch endpoints store outputs to a data store for further analysis. For more information on batch endpoints and deployments see [What are batch endpoints?](https://learn.microsoft.com/en-us/azure/machine-learning/concept-endpoints?view=azureml-api-2#what-are-batch-endpoints).\n", + "\n", + "* Create a batch endpoint.\n", + "* Create a batch deployment.\n", + "* Set the deployment as default; doing so allows invoking the endpoint without specifying the deployment's name." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "##### Create a batch endpoint" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import time, sys\n", + "from azure.ai.ml.entities import BatchEndpoint, BatchDeployment, BatchRetrySettings, AmlCompute\n", + "\n", + "# Create online endpoint - endpoint names need to be unique in a region, hence using timestamp to create unique endpoint name\n", + "endpoint_name = \"hf-image-classif-\" + str(timestamp)\n", + "# create a batch endpoint\n", + "endpoint = BatchEndpoint(\n", + " name=endpoint_name,\n", + " description=\"Batch endpoint for \" + foundation_model.name + \", for image-classification task\",\n", + ")\n", + "workspace_ml_client.begin_create_or_update(endpoint).result()" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "##### Create a batch deployment" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "deployment_name = \"demo\"\n", + "\n", + "deployment = BatchDeployment(\n", + " name=deployment_name,\n", + " endpoint_name=endpoint_name,\n", + " model=foundation_model.id,\n", + " compute=compute_name,\n", + " error_threshold=0,\n", + " instance_count=1,\n", + " logging_level=\"info\",\n", + " max_concurrency_per_instance=1,\n", + " mini_batch_size=2,\n", + " output_file_name=\"predictions.csv\",\n", + " retry_settings=BatchRetrySettings(max_retries=3, timeout=600),\n", + ")\n", + "workspace_ml_client.begin_create_or_update(deployment).result()" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Set the deployment as default" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "endpoint = workspace_ml_client.batch_endpoints.get(endpoint_name)\n", + "endpoint.defaults.deployment_name = deployment_name\n", + "workspace_ml_client.begin_create_or_update(endpoint).result()\n", + "\n", + "endpoint = workspace_ml_client.batch_endpoints.get(endpoint_name)\n", + "print(f\"The default deployment is {endpoint.defaults.deployment_name}\")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 5.1 Test the endpoint with Image Folder\n", + "\n", + "Invoke the batch endpoint with the input parameter pointing to the folder containing the batch inference input. This creates a pipeline job using the default deployment in the endpoint. Wait for the job to complete." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "input = Input(path=dataset_dir, type=AssetTypes.URI_FOLDER)\n", + "\n", + "job = workspace_ml_client.batch_endpoints.invoke(endpoint_name=endpoint.name, input=input)\n", + "\n", + "workspace_ml_client.jobs.stream(job.name)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "scoring_job = list(workspace_ml_client.jobs.list(parent_job_name=job.name))[0]\n", + "\n", + "workspace_ml_client.jobs.download(\n", + " name=scoring_job.name, download_path=os.path.join(dataset_parent_dir, \"image-folder-output\"), output_name=\"score\"\n", + ")\n", + "\n", + "predictions_file = os.path.join(dataset_parent_dir, \"image-folder-output\", \"named-outputs\", \"score\", \"predictions.csv\")\n", + "\n", + "# Load the batch predictions file with no headers into a dataframe and set your column names\n", + "score_df = pd.read_csv(\n", + " predictions_file,\n", + " header=None,\n", + " names=[\"row_number_per_file\", \"preds\", \"labels\", \"file_name\"],\n", + ")\n", + "score_df.head()" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 5.2 Test the endpoint with CSV input\n", + "\n", + "Invoke the batch endpoint with the input parameter pointing to the csv file containing the batch inference input. This creates a pipeline job using the default deployment in the endpoint. Wait for the job to complete." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "input = Input(path=csv_file_path, type=AssetTypes.URI_FILE)\n", + "\n", + "job = workspace_ml_client.batch_endpoints.invoke(endpoint_name=endpoint.name, input=input)\n", + "\n", + "workspace_ml_client.jobs.stream(job.name)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "scoring_job = list(workspace_ml_client.jobs.list(parent_job_name=job.name))[0]\n", + "\n", + "workspace_ml_client.jobs.download(\n", + " name=scoring_job.name, download_path=os.path.join(dataset_parent_dir, \"csv-output\"), output_name=\"score\"\n", + ")\n", + "\n", + "predictions_file = os.path.join(dataset_parent_dir, \"csv-output\", \"named-outputs\", \"score\", \"predictions.csv\")\n", + "\n", + "# Load the batch predictions file with no headers into a dataframe and set your column names\n", + "score_df = pd.read_csv(\n", + " predictions_file,\n", + " header=None,\n", + " names=[\"row_number_per_file\", \"preds\", \"labels\", \"file_name\"],\n", + ")\n", + "score_df.head()" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 6. Clean up resources\n", + "Batch endpoints use compute resources only when jobs are submitted. You can keep the batch endpoint for your reference without worrying about compute bills, or choose to delete the endpoint. If you created your compute cluster to have zero minimum instances and scale down soon after being idle, you won't be charged for an unused compute." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "workspace_ml_client.batch_endpoints.begin_delete(name=endpoint_name).result()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/sdk/python/foundation-models/system/inference/image-classification/image-classification-online-endpoint.ipynb b/sdk/python/foundation-models/system/inference/image-classification/image-classification-online-endpoint.ipynb index 380349b231..b78728dac6 100644 --- a/sdk/python/foundation-models/system/inference/image-classification/image-classification-online-endpoint.ipynb +++ b/sdk/python/foundation-models/system/inference/image-classification/image-classification-online-endpoint.ipynb @@ -70,9 +70,7 @@ " subscription_id = \"\"\n", " resource_group = \"\"\n", " workspace_name = \"\"\n", - "workspace_ml_client = MLClient(\n", - " credential, subscription_id, resource_group, workspace_name\n", - ")\n", + "workspace_ml_client = MLClient(credential, subscription_id, resource_group, workspace_name)\n", "\n", "# the models, fine tuning pipelines and environments are available in the AzureML system registry, \"azureml-preview\"\n", "registry_ml_client = MLClient(\n", @@ -83,7 +81,7 @@ " registry_name=\"azureml-preview\",\n", ")\n", "# genrating a unique timestamp that can be used for names and versions that need to be unique\n", - "timestamp = str(int(time.time())) \n" + "timestamp = str(int(time.time()))" ] }, { @@ -104,9 +102,11 @@ "source": [ "model_name = \"microsoft-beit-base-patch16-224-pt22k-ft22k\"\n", "model_version = \"1\"\n", - "foundation_model=registry_ml_client.models.get(model_name, model_version)\n", + "foundation_model = registry_ml_client.models.get(model_name, model_version)\n", "\n", - "print(f\"\\n\\nUsing model name: {foundation_model.name}, version: {foundation_model.version}, id: {foundation_model.id} for inferencing\")" + "print(\n", + " f\"\\n\\nUsing model name: {foundation_model.name}, version: {foundation_model.version}, id: {foundation_model.id} for inferencing\"\n", + ")" ] }, { @@ -201,7 +201,7 @@ "endpoint = ManagedOnlineEndpoint(\n", " name=online_endpoint_name,\n", " description=\"Online endpoint for \" + foundation_model.name + \", for image-classification task\",\n", - " auth_mode=\"key\"\n", + " auth_mode=\"key\",\n", ")\n", "workspace_ml_client.begin_create_or_update(endpoint).wait()" ] @@ -226,12 +226,10 @@ " endpoint_name=online_endpoint_name,\n", " model=foundation_model.id,\n", " # use GPU instance type like Standard_NC6s_v3 for faster explanations\n", - " instance_type=\"Standard_DS3_V2\", #\"Standard_DS3_V2\",\n", + " instance_type=\"Standard_DS3_V2\", # \"Standard_DS3_V2\",\n", " instance_count=1,\n", " request_settings=OnlineRequestSettings(\n", - " max_concurrent_requests_per_instance=1,\n", - " request_timeout_ms=5000, #90000,\n", - " max_queue_wait_ms=500\n", + " max_concurrent_requests_per_instance=1, request_timeout_ms=5000, max_queue_wait_ms=500 # 90000,\n", " ),\n", " liveness_probe=ProbeSettings(\n", " failure_threshold=30,\n", @@ -296,10 +294,12 @@ "\n", "sample_image = os.path.join(dataset_dir, \"milk_bottle\", \"99.jpg\")\n", "\n", + "\n", "def read_image(image_path):\n", " with open(image_path, \"rb\") as f:\n", " return f.read()\n", "\n", + "\n", "# {\"inputs\":{\"image\":[\"\"]}}\n", "request_json = {\n", " \"inputs\": {\n",