From 227786a2bfb3b6f870148e708bad04d86dc15668 Mon Sep 17 00:00:00 2001 From: Roope Astala Date: Wed, 19 Sep 2018 11:04:45 -0400 Subject: [PATCH] Update notebooks Update notebooks --- .../04.train-on-remote-vm.ipynb | 5 +- .../05.train-in-spark/05.train-in-spark.ipynb | 288 +----- .../07.hyperdrive-with-sklearn.ipynb | 4 +- automl/00.configuration.ipynb | 2 + automl/03b.auto-ml-remote-batchai.ipynb | 4 +- automl/13.auto-ml-dataprep.ipynb | 4 +- .../14a.auto-ml-classification-ensemble.ipynb | 412 --------- automl/14b.auto-ml-regression-ensemble.ipynb | 437 --------- automl/README.md | 4 +- automl/automl_setup.cmd | 3 +- automl/automl_setup_linux.sh | 2 +- automl/automl_setup_mac.sh | 2 +- onnx/onnx-inference-emotion-recognition.ipynb | 729 +++++++++++++++ onnx/onnx-inference-mnist.ipynb | 854 ++++++++++++++++++ ...ing.ipynb => pipeline-batch-scoring.ipynb} | 6 +- .../01.train-tune-deploy-pytorch.ipynb | 641 +++++++++++++ .../pytorch_score.py | 59 ++ .../pytorch_train.py | 169 ++++ .../02.distributed-pytorch-with-horovod.ipynb | 289 ++++++ .../pytorch_horovod_mnist.py | 157 ++++ ....distributed-tensorflow-with-horovod.ipynb | 360 ++++++++ .../tf_horovod_word2vec.py | 259 ++++++ ...ted-tensorflow-with-parameter-server.ipynb | 286 ++++++ .../tf_mnist_replica.py | 271 ++++++ ....distributed-cntk-with-custom-docker.ipynb | 283 ++++++ .../cntk_mnist.py | 321 +++++++ .../07.tensorboard.ipynb} | 4 +- ...8.export-run-history-to-tensorboard.ipynb} | 0 ....distributed-tensorflow-with-horovod.ipynb | 500 ---------- ...ted-tensorflow-with-parameter-server.ipynb | 473 ---------- .../52.distributed-cntk.ipynb | 509 ----------- .../53.distributed-pytorch-with-horovod.ipynb | 376 -------- tutorials/01.train-models.ipynb | 4 +- 33 files changed, 4752 insertions(+), 2965 deletions(-) delete mode 100644 automl/14a.auto-ml-classification-ensemble.ipynb delete mode 100644 automl/14b.auto-ml-regression-ensemble.ipynb create mode 100644 onnx/onnx-inference-emotion-recognition.ipynb create mode 100644 onnx/onnx-inference-mnist.ipynb rename pipeline/{06.pipeline-batch-scoring.ipynb => pipeline-batch-scoring.ipynb} (99%) create mode 100644 training/01.train-tune-deploy-pytorch/01.train-tune-deploy-pytorch.ipynb create mode 100644 training/01.train-tune-deploy-pytorch/pytorch_score.py create mode 100644 training/01.train-tune-deploy-pytorch/pytorch_train.py create mode 100644 training/02.distributed-pytorch-with-horovod/02.distributed-pytorch-with-horovod.ipynb create mode 100644 training/02.distributed-pytorch-with-horovod/pytorch_horovod_mnist.py create mode 100644 training/04.distributed-tensorflow-with-horovod/04.distributed-tensorflow-with-horovod.ipynb create mode 100644 training/04.distributed-tensorflow-with-horovod/tf_horovod_word2vec.py create mode 100644 training/05.distributed-tensorflow-with-parameter-server/05.distributed-tensorflow-with-parameter-server.ipynb create mode 100644 training/05.distributed-tensorflow-with-parameter-server/tf_mnist_replica.py create mode 100644 training/06.distributed-cntk-with-custom-docker/06.distributed-cntk-with-custom-docker.ipynb create mode 100644 training/06.distributed-cntk-with-custom-docker/cntk_mnist.py rename training/{40.tensorboard/40.tensorboard.ipynb => 07.tensorboard/07.tensorboard.ipynb} (99%) rename training/{41.export-run-history-to-tensorboard/41.export-run-history-to-tensorboard.ipynb => 08.export-run-history-to-tensorboard/08.export-run-history-to-tensorboard.ipynb} (100%) delete mode 100644 training/50.distributed-tensorflow-with-horovod/50.distributed-tensorflow-with-horovod.ipynb delete mode 100644 training/51.distributed-tensorflow-with-parameter-server/51.distributed-tensorflow-with-parameter-server.ipynb delete mode 100644 training/52.distributed-cntk/52.distributed-cntk.ipynb delete mode 100644 training/53.distributed-pytorch-with-horovod/53.distributed-pytorch-with-horovod.ipynb diff --git a/01.getting-started/04.train-on-remote-vm/04.train-on-remote-vm.ipynb b/01.getting-started/04.train-on-remote-vm/04.train-on-remote-vm.ipynb index 9713264cd..4576d155d 100644 --- a/01.getting-started/04.train-on-remote-vm/04.train-on-remote-vm.ipynb +++ b/01.getting-started/04.train-on-remote-vm/04.train-on-remote-vm.ipynb @@ -195,9 +195,10 @@ "metadata": {}, "outputs": [], "source": [ - " '''\n", + "'''\n", " from azureml.core.compute import RemoteCompute \n", - " dsvm_compute = RemoteCompute.attach(ws,name=\"attach-from-sdk6\",username=,address=,ssh_port=22,password=)\n", + " # if you want to connect using SSH key instead of username/password you can provide parameters private_key_file and private_key_passphrase \n", + " dsvm_compute = RemoteCompute.attach(ws,name=\"attach-from-sdk6\",username=,address=,ssh_port=22,password=)\n", "'''" ] }, diff --git a/01.getting-started/05.train-in-spark/05.train-in-spark.ipynb b/01.getting-started/05.train-in-spark/05.train-in-spark.ipynb index 83ad121e4..6ba366a45 100644 --- a/01.getting-started/05.train-in-spark/05.train-in-spark.ipynb +++ b/01.getting-started/05.train-in-spark/05.train-in-spark.ipynb @@ -15,11 +15,9 @@ "source": [ "# 05. Train in Spark\n", "* Create Workspace\n", - "* Create Project\n", - "* Create `train-spark.py` file in the project folder\n", - "* Execute a PySpark script in ACI.\n", - "* Execute a PySpark script in a Docker container on remote DSVM\n", - "* Execute a PySpark script in HDI" + "* Create Experiment\n", + "* Copy relevant files to the script folder\n", + "* Configure and Run" ] }, { @@ -67,8 +65,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Create Project and Associate with Run History\n", - "**Project** is a local folder that contains files for your Azure ML experiments. It is associated with a **run history**, a cloud container of run metrics and output artifacts from your experiments. You can either attach a local folder as a new project, or load a local folder as a project if it has been attached before." + "## Create Experiment\n" ] }, { @@ -77,27 +74,15 @@ "metadata": {}, "outputs": [], "source": [ - "# choose a name for the run history container in the workspace\n", - "experiment_name = 'train-on-spark'\n", + "experiment_name = 'train-on-remote-vm'\n", + "script_folder = './samples/train-on-remote-vm'\n", "\n", - "# project folder\n", - "project_folder = './sample_projects/train-on-spark'" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ "import os\n", - "from azureml.project.project import Project\n", + "os.makedirs(script_folder, exist_ok = True)\n", "\n", - "project = Project.attach(workspace_object = ws,\n", - " experiment_name = experiment_name,\n", - " directory = project_folder)\n", + "from azureml.core import Experiment\n", "\n", - "print(project.project_directory, project.history.name, sep = '\\n')" + "exp = Experiment(workspace = ws, name = experiment_name)" ] }, { @@ -119,11 +104,11 @@ "from shutil import copyfile\n", "\n", "# copy iris dataset in to project folder\n", - "copyfile('./iris.csv', os.path.join(project_folder, 'iris.csv'))\n", + "copyfile('iris.csv', os.path.join(script_folder, 'iris.csv'))\n", "\n", "# copy train-spark.py file into project folder\n", "# train-spark.py trains a simple LogisticRegression model using Spark.ML algorithm\n", - "copyfile('./train-spark.py', os.path.join(project_folder, 'train-spark.py'))" + "copyfile('train-spark.py', os.path.join(script_folder, 'train-spark.py'))" ] }, { @@ -154,117 +139,10 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Configure ACI target" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from azureml.core.runconfig import RunConfiguration\n", - "from azureml.core.conda_dependencies import CondaDependencies\n", - "\n", - "# create a new runconfig object\n", - "run_config = RunConfiguration()\n", - "\n", - "# signal that you want to use ACI to execute script.\n", - "run_config.target = \"containerinstance\"\n", - "\n", - "# ACI container group is only supported in certain regions, which can be different than the region the Workspace is in.\n", - "run_config.container_instance.region = 'eastus'\n", - "\n", - "# set the ACI CPU and Memory \n", - "run_config.container_instance.cpu_cores = 1\n", - "run_config.container_instance.memory_gb = 2\n", - "\n", - "# enable Docker \n", - "run_config.environment.docker.enabled = True\n", - "\n", - "# set Docker base image to the default CPU-based image\n", - "run_config.environment.docker.base_image = azureml.core.runconfig.DEFAULT_MMLSPARK_CPU_IMAGE\n", - "print('base image is', run_config.environment.docker.base_image)\n", - "#run_config.environment.docker.base_image = 'microsoft/mmlspark:plus-0.9.9'\n", - "\n", - "# use conda_dependencies.yml to create a conda environment in the Docker image for execution\n", - "# please update this file if you need additional packages.\n", - "run_config.environment.python.user_managed_dependencies = False\n", - "\n", - "# auto-prepare the Docker image when used for execution (if it is not already prepared)\n", - "run_config.auto_prepare_environment = True\n", - "\n", - "cd = CondaDependencies()\n", - "# add numpy as a dependency\n", - "cd.add_conda_package('numpy')\n", - "# overwrite the default conda_dependencies.yml file\n", - "cd.save_to_file(base_directory = project_folder, conda_file_path='aml_config/conda_dependencies.yml')\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Run Spark job in ACI" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%%time \n", - "from azureml.core.experiment import Experiment\n", - "from azureml.core.script_run_config import ScriptRunConfig\n", - "\n", - "experiment = Experiment(project_object.workspace_object, project_object.history.name)\n", - "script_run_config = ScriptRunConfig(source_directory = project.project_directory,\n", - " script= 'train-spark.py',\n", - " run_config = run_config)\n", - "run = experiment.submit(script_run_config)\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "run.wait_for_completion(show_output = True)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Show the run in the web UI\n", - "**IMPORTANT**: Please use Chrome to navigate to the URL." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# import helpers.py\n", - "import helpers\n", - "\n", - "# get the URL of the run history web page\n", - "print(helpers.get_run_history_url(run))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Attach a remote Linux VM\n", - "To use remote docker commpute target:\n", - " 1. Create a Linux DSVM in Azure. Here is some [quick instructions](https://docs.microsoft.com/en-us/azure/machine-learning/desktop-workbench/how-to-create-dsvm-hdi). Make sure you use the Ubuntu flavor, NOT CentOS.\n", - " 2. Enter the IP address, username and password below\n", - " \n", - "**Note**: the below example use port 5022. By default SSH runs on port 22 and you don't need to specify it. But if for security reasons you switch to a different port (such as 5022), you can append the port number to the address like the example below. [Read more](../../documentation/sdk/ssh-issue.md) on this." + "### Attach an HDI cluster\n", + "To use HDI commpute target:\n", + " 1. Create an Spark for HDI cluster in Azure. Here is some [quick instructions](https://docs.microsoft.com/en-us/azure/machine-learning/desktop-workbench/how-to-create-dsvm-hdi). Make sure you use the Ubuntu flavor, NOT CentOS.\n", + " 2. Enter the IP address, username and password below" ] }, { @@ -273,25 +151,30 @@ "metadata": {}, "outputs": [], "source": [ - "from azureml.core.compute import RemoteCompute\n", + "from azureml.core.compute import HDInsightCompute\n", "\n", "try:\n", - " # Attaches a remote docker on a remote vm as a compute target.\n", - " RemoteCompute.attach(workspace,name = \"cpu-dsvm\", username = \"ninghai\", \n", - " address = \"hai2.eastus2.cloudapp.azure.com:5022\", \n", - " ssh-port=22\n", - " password = \"\"))\n", + " # if you want to connect using SSH key instead of username/password you can provide parameters private_key_file and private_key_passphrase\n", + " hdi_compute_new = HDInsightCompute.attach(ws, \n", + " name=\"hdi-attach\", \n", + " address=\"hdi-ignite-demo-ssh.azurehdinsight.net\", \n", + " ssh_port=22, \n", + " username='', \n", + " password='')\n", + "\n", "except UserErrorException as e:\n", " print(\"Caught = {}\".format(e.message))\n", - " print(\"Compute config already attached.\")" + " print(\"Compute config already attached.\")\n", + " \n", + " \n", + "hdi_compute_new.wait_for_completion(show_output=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### Configure a Spark Docker run on the VM\n", - "Execute in the Spark engine in a Docker container in the VM. " + "### Configure HDI run" ] }, { @@ -300,107 +183,32 @@ "metadata": {}, "outputs": [], "source": [ + "from azureml.core.runconfig import RunConfiguration\n", + "from azureml.core.conda_dependencies import CondaDependencies\n", + "\n", + "\n", "# Load the \"cpu-dsvm.runconfig\" file (created by the above attach operation) in memory\n", - "run_config = RunConfiguration.load(path = project_folder, name = \"cpu-dsvm\")\n", + "run_config = RunConfiguration(framework = \"python\")\n", "\n", - "# set framework to PySpark\n", - "run_config.framework = \"PySpark\"\n", + "# Set compute target to the Linux DSVM\n", + "run_config.target = hdi_compute.name\n", "\n", "# Use Docker in the remote VM\n", - "run_config.environment.docker.enabled = True\n", + "# run_config.environment.docker.enabled = True\n", "\n", - "# Use the MMLSpark CPU based image.\n", - "# https://hub.docker.com/r/microsoft/mmlspark/\n", - "run_config.environment.docker.base_image = azureml.core.runconfig.DEFAULT_MMLSPARK_CPU_IMAGE\n", - "print('base image is:', run_config.environment.docker.base_image)\n", + "# Use CPU base image from DockerHub\n", + "# run_config.environment.docker.base_image = azureml.core.runconfig.DEFAULT_CPU_IMAGE\n", + "# print('Base Docker image is:', run_config.environment.docker.base_image)\n", "\n", - "# signal use the user-managed environment\n", - "# do NOT provision a new one based on the conda.yml file\n", + "# Ask system to provision a new one based on the conda_dependencies.yml file\n", "run_config.environment.python.user_managed_dependencies = False\n", "\n", - "# Prepare the Docker and conda environment automatically when execute for the first time.\n", - "run_config.auto_prepare_environment = True" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Submit the Experiment\n", - "Submit script to run in the Spark engine in the Docker container in the remote VM." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "script_run_config = ScriptRunConfig(source_directory = project.project_directory,\n", - " script= 'train-spark.py',\n", - " run_config = run_config)\n", - "run = experiment.submit(script_run_config)\n", - "\n", - "run.wait_for_completion(show_output = True)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# get the URL of the run history web page\n", - "print(helpers.get_run_history_url(run))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Attach an HDI cluster\n", - "To use HDI commpute target:\n", - " 1. Create an Spark for HDI cluster in Azure. Here is some [quick instructions](https://docs.microsoft.com/en-us/azure/machine-learning/desktop-workbench/how-to-create-dsvm-hdi). Make sure you use the Ubuntu flavor, NOT CentOS.\n", - " 2. Enter the IP address, username and password below" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from azureml.core.compute import HDInsightCompute\n", - "\n", - "try:\n", - " # Attaches a HDI cluster as a compute target.\n", - " HDInsightCompute.attach(ws, name = \"myhdi\",\n", - " username = \"ninghai\", \n", - " address = \"sparkhai-ssh.azurehdinsight.net\", \n", - " password = \"\"))\n", - "except UserErrorException as e:\n", - " print(\"Caught = {}\".format(e.message))\n", - " print(\"Compute config already attached.\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Configure HDI run" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# load the runconfig object from the \"myhdi.runconfig\" file generated by the attach operaton above.\n", - "run_config = RunConfiguration.load(path = project_folder, name = 'myhdi')\n", + "# Prepare the Docker and conda environment automatically when executingfor the first time.\n", + "# run_config.prepare_environment = True\n", "\n", - "# ask system to prepare the conda environment automatically when executed for the first time\n", - "run_config.auto_prepare_environment = True" + "# specify CondaDependencies obj\n", + "# run_config.environment.python.conda_dependencies = CondaDependencies.create(conda_packages=['scikit-learn'])\n", + "# load the runconfig object from the \"myhdi.runconfig\" file generated by the attach operaton above." ] }, { @@ -448,7 +256,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python [default]", "language": "python", "name": "python3" }, @@ -462,7 +270,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.5" + "version": "3.6.6" } }, "nbformat": 4, diff --git a/01.getting-started/07.hyperdrive-with-sklearn/07.hyperdrive-with-sklearn.ipynb b/01.getting-started/07.hyperdrive-with-sklearn/07.hyperdrive-with-sklearn.ipynb index beec8dc59..26e80c05c 100644 --- a/01.getting-started/07.hyperdrive-with-sklearn/07.hyperdrive-with-sklearn.ipynb +++ b/01.getting-started/07.hyperdrive-with-sklearn/07.hyperdrive-with-sklearn.ipynb @@ -109,7 +109,9 @@ "metadata": {}, "source": [ "## Provision New Cluster\n", - "Create a new Batch AI cluster using the following Python code." + "Create a new Batch AI cluster using the following Python code.\n", + "\n", + "**Note**: As with other Azure services, there are limits on certain resources (for eg. BatchAI cluster size) associated with the Azure Machine Learning service. Please read [this article](https://docs.microsoft.com/en-us/azure/machine-learning/service/how-to-manage-quotas) on the default limits and how to request more quota." ] }, { diff --git a/automl/00.configuration.ipynb b/automl/00.configuration.ipynb index f056c79f1..a499ddb15 100644 --- a/automl/00.configuration.ipynb +++ b/automl/00.configuration.ipynb @@ -181,6 +181,8 @@ "metadata": {}, "outputs": [], "source": [ + "from azureml.core import Workspace\n", + "\n", "ws = Workspace(workspace_name = workspace_name,\n", " subscription_id = subscription_id,\n", " resource_group = resource_group)\n", diff --git a/automl/03b.auto-ml-remote-batchai.ipynb b/automl/03b.auto-ml-remote-batchai.ipynb index 8802127e7..8fc93fca9 100644 --- a/automl/03b.auto-ml-remote-batchai.ipynb +++ b/automl/03b.auto-ml-remote-batchai.ipynb @@ -120,7 +120,9 @@ "## Create Batch AI Cluster\n", "The cluster is created as Machine Learning Compute and will appear under your workspace.\n", "\n", - "Note: The cluster creation can take over 10 minutes, be patient." + "Note: The cluster creation can take over 10 minutes, please be patient.\n", + "\n", + "As with other Azure services, there are limits on certain resources (for eg. BatchAI cluster size) associated with the Azure Machine Learning service. Please read [this article](https://docs.microsoft.com/en-us/azure/machine-learning/service/how-to-manage-quotas) on the default limits and how to request more quota." ] }, { diff --git a/automl/13.auto-ml-dataprep.ipynb b/automl/13.auto-ml-dataprep.ipynb index 5da708f83..1d8bea42b 100644 --- a/automl/13.auto-ml-dataprep.ipynb +++ b/automl/13.auto-ml-dataprep.ipynb @@ -46,7 +46,7 @@ "metadata": {}, "outputs": [], "source": [ - "!pip install --upgrade --extra-index-url https://dataprepdownloads.azureedge.net/pypi/monthly-AE98437A2C8F6F45842C/latest azureml-dataprep --no-cache-dir --force-reinstall\n", + "!pip install --upgrade --extra-index-url https://dataprepdownloads.azureedge.net/pypi/autoML-BD0E9CABED27C837/0.1.1809.11043 azureml-dataprep --no-cache-dir --force-reinstall\n", "!pip install tornado==4.5.1" ] }, @@ -279,7 +279,7 @@ "source": [ "cd = CondaDependencies()\n", "cd.set_pip_index_url(index_url=\"--index-url https://azuremlsdktestpypi.azureedge.net/sdk-release/master/588E708E0DF342C4A80BD954289657CF\")\n", - "cd.set_pip_index_url(index_url=\"--extra-index-url https://dataprepdownloads.azureedge.net/pypi/monthly-AE98437A2C8F6F45842C/latest --extra-index-url https://pypi.python.org/simple\")\n", + "cd.set_pip_index_url(index_url=\"--extra-index-url https://dataprepdownloads.azureedge.net/pypi/autoML-BD0E9CABED27C837/0.1.1809.11043 --extra-index-url https://pypi.python.org/simple\")\n", "cd.remove_pip_package(pip_package=\"azureml-defaults\")\n", "cd.add_pip_package(pip_package='azureml-core')\n", "cd.add_pip_package(pip_package='azureml-telemetry')\n", diff --git a/automl/14a.auto-ml-classification-ensemble.ipynb b/automl/14a.auto-ml-classification-ensemble.ipynb deleted file mode 100644 index 87d633dff..000000000 --- a/automl/14a.auto-ml-classification-ensemble.ipynb +++ /dev/null @@ -1,412 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Copyright (c) Microsoft Corporation. All rights reserved.\n", - "\n", - "Licensed under the MIT License." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# AutoML 01: Classification with ensembling on local compute\n", - "\n", - "In this example we use the scikit learn's [digit dataset](http://scikit-learn.org/stable/modules/generated/sklearn.datasets.load_digits.html) to showcase how you can use the AutoML Classifier for a simple classification problem.\n", - "\n", - "Make sure you have executed the [00.configuration](00.configuration.ipynb) before running this notebook.\n", - "\n", - "In this notebook you would see\n", - "1. Creating an Experiment in an existing Workspace\n", - "2. Instantiating AutoMLConfig\n", - "3. Training the Model using local compute\n", - "4. Exploring the results\n", - "5. Testing the fitted model\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Create Experiment\n", - "\n", - "As part of the setup you have already created a Workspace. For AutoML you would need to create an Experiment. An Experiment is a named object in a Workspace, which is used to run experiments." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import logging\n", - "import os\n", - "import random\n", - "\n", - "from matplotlib import pyplot as plt\n", - "from matplotlib.pyplot import imshow\n", - "import numpy as np\n", - "import pandas as pd\n", - "from sklearn import datasets\n", - "\n", - "import azureml.core\n", - "from azureml.core.experiment import Experiment\n", - "from azureml.core.workspace import Workspace\n", - "from azureml.train.automl import AutoMLConfig\n", - "from azureml.train.automl.run import AutoMLRun" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "ws = Workspace.from_config()\n", - "\n", - "# choose a name for experiment\n", - "experiment_name = 'automl-local-classification'\n", - "# project folder\n", - "project_folder = './sample_projects/automl-local-classification'\n", - "\n", - "experiment=Experiment(ws, experiment_name)\n", - "\n", - "output = {}\n", - "output['SDK version'] = azureml.core.VERSION\n", - "output['Subscription ID'] = ws.subscription_id\n", - "output['Workspace Name'] = ws.name\n", - "output['Resource Group'] = ws.resource_group\n", - "output['Location'] = ws.location\n", - "output['Project Directory'] = project_folder\n", - "output['Experiment Name'] = experiment.name\n", - "pd.set_option('display.max_colwidth', -1)\n", - "pd.DataFrame(data = output, index = ['']).T" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Diagnostics\n", - "\n", - "Opt-in diagnostics for better experience, quality, and security of future releases" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from azureml.telemetry import set_diagnostics_collection\n", - "set_diagnostics_collection(send_diagnostics=True)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Load Digits Dataset" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from sklearn import datasets\n", - "\n", - "digits = datasets.load_digits()\n", - "\n", - "X_digits = digits.data[100:,:]\n", - "y_digits = digits.target[100:]\n", - "X_valid = digits.data[0:100]\n", - "y_valid = digits.target[0:100]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Instantiate Auto ML Config\n", - "\n", - "Instantiate a AutoMLConfig object. This defines the settings and data used to run the experiment.\n", - "\n", - "|Property|Description|\n", - "|-|-|\n", - "|**task**|classification or regression|\n", - "|**primary_metric**|This is the metric that you want to optimize.
Classification supports the following primary metrics
accuracy
AUC_weighted
balanced_accuracy
average_precision_score_weighted
precision_score_weighted|\n", - "|**max_time_sec**|Time limit in seconds for each iterations|\n", - "|**iterations**|Number of iterations. In each iteration Auto ML trains the data with a specific pipeline|\n", - "|**n_cross_validations**|Number of cross validation splits|\n", - "|**X**|(sparse) array-like, shape = [n_samples, n_features]|\n", - "|**y**|(sparse) array-like, shape = [n_samples, ], [n_samples, n_classes]
Multi-class targets. An indicator matrix turns on multilabel classification. This should be an array of integers. |\n", - "|**X_valid**|(sparse) array-like, shape = [n_samples, n_features]|\n", - "|**y_valid**|(sparse) array-like, shape = [n_samples, ], [n_samples, n_classes]
Multi-class targets. An indicator matrix turns on multilabel classification. This should be an array of integers. |\n", - "|**enable_ensembling**|Flag to enable an ensembling iteration after all the other iterations complete|\n", - "|**ensemble_iterations**|Number of iterations during which we choose a fitted model to be part of the final ensemble|" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "automl_config = AutoMLConfig(task = 'classification',\n", - " debug_log = 'automl_errors.log',\n", - " primary_metric = 'AUC_weighted',\n", - " max_time_sec = 12000,\n", - " iterations = 10,\n", - " verbosity = logging.INFO,\n", - " X = X_digits, \n", - " y = y_digits,\n", - " X_valid = X_valid,\n", - " y_valid = y_valid,\n", - " enable_ensembling = True,\n", - " ensemble_iterations = 5)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Training the Model\n", - "\n", - "You can call the submit method on the experiment object and pass the run configuration. For Local runs the execution is synchronous. Depending on the data and number of iterations this can run for while.\n", - "You will see the currently running iterations printing to the console." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "local_run = experiment.submit(automl_config, show_output=True)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Optionally, you can continue an interrupted local run by calling continue_experiment without the iterations parameter, or run more iterations to a completed run by specifying the iterations parameter:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "local_run" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "local_run = local_run.continue_experiment(X = X_digits, \n", - " y = y_digits, \n", - " show_output = True,\n", - " iterations = 5)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Exploring the results" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Widget for monitoring runs\n", - "\n", - "The widget will sit on \"loading\" until the first iteration completed, then you will see an auto-updating graph and table show up. It refreshed once per minute, so you should see the graph update as child runs complete.\n", - "\n", - "NOTE: The widget displays a link at the bottom. This links to a web-ui to explore the individual run details." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from azureml.train.widgets import RunDetails\n", - "RunDetails(local_run).show() " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "#### Retrieve All Child Runs\n", - "You can also use sdk methods to fetch all the child runs and see individual metrics that we log. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "children = list(local_run.get_children())\n", - "metricslist = {}\n", - "for run in children:\n", - " properties = run.get_properties()\n", - " metrics = {k: v for k, v in run.get_metrics().items() if isinstance(v, float)} \n", - " metricslist[int(properties['iteration'])] = metrics\n", - "\n", - "rundata = pd.DataFrame(metricslist).sort_index(1)\n", - "rundata" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Retrieve the Best Model\n", - "\n", - "Below we select the best pipeline from our iterations. The *get_output* method on automl_classifier returns the best run and the fitted model for the last *fit* invocation. There are overloads on *get_output* that allow you to retrieve the best run and fitted model for *any* logged metric or a particular *iteration*." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "best_run, fitted_model = local_run.get_output()\n", - "print(best_run)\n", - "print(fitted_model)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Best Model based on any other metric\n", - "Give me the run and the model that has the smallest `log_loss`:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "lookup_metric = \"log_loss\"\n", - "best_run, fitted_model = local_run.get_output(metric = lookup_metric)\n", - "print(best_run)\n", - "print(fitted_model)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Best Model based on any iteration\n", - "Give me the run and the model from the 3rd iteration:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "iteration = 3\n", - "best_run, fitted_model = local_run.get_output(iteration = iteration)\n", - "print(best_run)\n", - "print(fitted_model)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Testing the Fitted Model \n", - "\n", - "#### Load Test Data" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "digits = datasets.load_digits()\n", - "X_digits = digits.data[:10, :]\n", - "y_digits = digits.target[:10]\n", - "images = digits.images[:10]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Testing our best pipeline\n", - "We will try to predict 2 digits and see how our model works." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#Randomly select digits and test\n", - "for index in np.random.choice(len(y_digits), 2):\n", - " print(index)\n", - " predicted = fitted_model.predict(X_digits[index:index + 1])[0]\n", - " label = y_digits[index]\n", - " title = \"Label value = %d Predicted value = %d \" % ( label,predicted)\n", - " fig = plt.figure(1, figsize=(3,3))\n", - " ax1 = fig.add_axes((0,0,.8,.8))\n", - " ax1.set_title(title)\n", - " plt.imshow(images[index], cmap=plt.cm.gray_r, interpolation='nearest')\n", - " plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python [default]", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.6" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/automl/14b.auto-ml-regression-ensemble.ipynb b/automl/14b.auto-ml-regression-ensemble.ipynb deleted file mode 100644 index 921c2a4d5..000000000 --- a/automl/14b.auto-ml-regression-ensemble.ipynb +++ /dev/null @@ -1,437 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Copyright (c) Microsoft Corporation. All rights reserved.\n", - "\n", - "Licensed under the MIT License." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# AutoML 02: Regression with ensembling on local compute\n", - "\n", - "In this example we use the scikit learn's [diabetes dataset](http://scikit-learn.org/stable/modules/generated/sklearn.datasets.load_diabetes.html) to showcase how you can use the AutoML for a simple regression problem.\n", - "\n", - "Make sure you have executed the [00.configuration](00.configuration.ipynb) before running this notebook.\n", - "\n", - "In this notebook you would see\n", - "1. Creating an Experiment using an existing Workspace\n", - "2. Instantiating AutoMLConfig\n", - "3. Training the Model using local compute\n", - "4. Exploring the results\n", - "5. Testing the fitted model" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Create Experiment\n", - "\n", - "As part of the setup you have already created a Workspace. For AutoML you would need to create an Experiment. An Experiment is a named object in a Workspace, which is used to run experiments." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import logging\n", - "import os\n", - "import random\n", - "\n", - "from matplotlib import pyplot as plt\n", - "from matplotlib.pyplot import imshow\n", - "import numpy as np\n", - "import pandas as pd\n", - "from sklearn import datasets\n", - "\n", - "import azureml.core\n", - "from azureml.core.experiment import Experiment\n", - "from azureml.core.workspace import Workspace\n", - "from azureml.train.automl import AutoMLConfig\n", - "from azureml.train.automl.run import AutoMLRun" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "ws = Workspace.from_config()\n", - "\n", - "# choose a name for the experiment\n", - "experiment_name = 'automl-local-regression'\n", - "# project folder\n", - "project_folder = './sample_projects/automl-local-regression'\n", - "\n", - "experiment = Experiment(ws, experiment_name)\n", - "\n", - "output = {}\n", - "output['SDK version'] = azureml.core.VERSION\n", - "output['Subscription ID'] = ws.subscription_id\n", - "output['Workspace Name'] = ws.name\n", - "output['Resource Group'] = ws.resource_group\n", - "output['Location'] = ws.location\n", - "output['Project Directory'] = project_folder\n", - "output['Experiment Name'] = experiment.name\n", - "pd.set_option('display.max_colwidth', -1)\n", - "pd.DataFrame(data = output, index = ['']).T" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Diagnostics\n", - "\n", - "Opt-in diagnostics for better experience, quality, and security of future releases" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from azureml.telemetry import set_diagnostics_collection\n", - "set_diagnostics_collection(send_diagnostics=True)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Read Data" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# load diabetes dataset, a well-known built-in small dataset that comes with scikit-learn\n", - "from sklearn.datasets import load_diabetes\n", - "from sklearn.linear_model import Ridge\n", - "from sklearn.metrics import mean_squared_error\n", - "from sklearn.model_selection import train_test_split\n", - "\n", - "X, y = load_diabetes(return_X_y = True)\n", - "\n", - "columns = ['age', 'gender', 'bmi', 'bp', 's1', 's2', 's3', 's4', 's5', 's6']\n", - "\n", - "x_train, x_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=0)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Instantiate Auto ML Config\n", - "\n", - "Instantiate a AutoMLConfig object. This defines the settings and data used to run the experiment.\n", - "\n", - "|Property|Description|\n", - "|-|-|\n", - "|**task**|classification or regression|\n", - "|**primary_metric**|This is the metric that you want to optimize.
Regression supports the following primary metrics
spearman_correlation
normalized_root_mean_squared_error
r2_score
normalized_mean_absolute_error
normalized_root_mean_squared_log_error|\n", - "|**max_time_sec**|Time limit in seconds for each iterations|\n", - "|**iterations**|Number of iterations. In each iteration Auto ML Classifier trains the data with a specific pipeline|\n", - "|**n_cross_validations**|Number of cross validation splits|\n", - "|**X**|(sparse) array-like, shape = [n_samples, n_features]|\n", - "|**y**|(sparse) array-like, shape = [n_samples, ], [n_samples, n_classes]
Multi-class targets. An indicator matrix turns on multilabel classification. This should be an array of integers. |\n", - "|**enable_ensembling**|Flag to enable an ensembling iteration after all the other iterations complete|\n", - "|**ensemble_iterations**|Number of iterations during which we choose a fitted model to be part of the final ensemble|" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "automl_config = AutoMLConfig(task='regression',\n", - " max_time_sec = 600,\n", - " iterations = 10,\n", - " primary_metric = 'spearman_correlation', \n", - " debug_log = 'automl.log',\n", - " verbosity = logging.INFO,\n", - " X = x_train, \n", - " y = y_train,\n", - " X_valid = x_test,\n", - " y_valid = y_test,\n", - " enable_ensembling = True,\n", - " ensemble_iterations = 5)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Training the Model\n", - "\n", - "You can call the submit method on the experiment object and pass the run configuration. For Local runs the execution is synchronous. Depending on the data and number of iterations this can run for while.\n", - "You will see the currently running iterations printing to the console." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "local_run = experiment.submit(automl_config, show_output=True)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "local_run" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Exploring the results" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Widget for monitoring runs\n", - "\n", - "The widget will sit on \"loading\" until the first iteration completed, then you will see an auto-updating graph and table show up. It refreshed once per minute, so you should see the graph update as child runs complete.\n", - "\n", - "NOTE: The widget displays a link at the bottom. This links to a web-ui to explore the individual run details." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from azureml.train.widgets import RunDetails\n", - "RunDetails(local_run).show() " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "#### Retrieve All Child Runs\n", - "You can also use sdk methods to fetch all the child runs and see individual metrics that we log. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "children = list(local_run.get_children())\n", - "metricslist = {}\n", - "for run in children:\n", - " properties = run.get_properties()\n", - " metrics = {k: v for k, v in run.get_metrics().items() if isinstance(v, float)} \n", - " metricslist[int(properties['iteration'])] = metrics\n", - " \n", - "rundata = pd.DataFrame(metricslist).sort_index(1)\n", - "rundata" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Retrieve the Best Model\n", - "\n", - "Below we select the best pipeline from our iterations. The *get_output* method on automl_classifier returns the best run and the fitted model for the last *fit* invocation. There are overloads on *get_output* that allow you to retrieve the best run and fitted model for *any* logged metric or a particular *iteration*." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "best_run, fitted_model = local_run.get_output()\n", - "print(best_run)\n", - "print(fitted_model)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Best Model based on any other metric\n", - "Show the run and model that has the smallest `root_mean_squared_error` (which turned out to be the same as the one with largest `spearman_correlation` value):" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "lookup_metric = \"root_mean_squared_error\"\n", - "best_run, fitted_model = local_run.get_output(metric=lookup_metric)\n", - "print(best_run)\n", - "print(fitted_model)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Best Model based on any iteration\n", - "Simply show the run and model from the 3rd iteration:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "iteration = 3\n", - "third_run, third_model = local_run.get_output(iteration = iteration)\n", - "print(third_run)\n", - "print(third_model)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Register fitted model for deployment" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "description = 'AutoML Model'\n", - "tags = None\n", - "local_run.register_model(description = description, tags = tags)\n", - "print(local_run.model_id) # Use this id to deploy the model as a web service in Azure" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Testing the Fitted Model" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Predict on training and test set, and calculate residual values." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "y_pred_train = fitted_model.predict(x_train)\n", - "y_residual_train = y_train - y_pred_train\n", - "\n", - "y_pred_test = fitted_model.predict(x_test)\n", - "y_residual_test = y_test - y_pred_test" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%matplotlib inline\n", - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "from sklearn import datasets\n", - "from sklearn.metrics import mean_squared_error, r2_score\n", - "\n", - "# set up a multi-plot chart\n", - "f, (a0, a1) = plt.subplots(1, 2, gridspec_kw = {'width_ratios':[1, 1], 'wspace':0, 'hspace': 0})\n", - "f.suptitle('Regression Residual Values', fontsize = 18)\n", - "f.set_figheight(6)\n", - "f.set_figwidth(16)\n", - "\n", - "# plot residual values of training set\n", - "a0.axis([0, 360, -200, 200])\n", - "a0.plot(y_residual_train, 'bo', alpha = 0.5)\n", - "a0.plot([-10,360],[0,0], 'r-', lw = 3)\n", - "a0.text(16,170,'RMSE = {0:.2f}'.format(np.sqrt(mean_squared_error(y_train, y_pred_train))), fontsize = 12)\n", - "a0.text(16,140,'Variance = {0:.2f}'.format(r2_score(y_train, y_pred_train)), fontsize = 12)\n", - "a0.set_xlabel('Training samples', fontsize = 12)\n", - "a0.set_ylabel('Residual Values', fontsize = 12)\n", - "# plot histogram\n", - "a0.hist(y_residual_train, orientation = 'horizontal', color = 'b', bins = 10, histtype = 'step');\n", - "a0.hist(y_residual_train, orientation = 'horizontal', color = 'b', alpha = 0.2, bins = 10);\n", - "\n", - "# plot residual values of test set\n", - "a1.axis([0, 90, -200, 200])\n", - "a1.plot(y_residual_test, 'bo', alpha = 0.5)\n", - "a1.plot([-10,360],[0,0], 'r-', lw = 3)\n", - "a1.text(5,170,'RMSE = {0:.2f}'.format(np.sqrt(mean_squared_error(y_test, y_pred_test))), fontsize = 12)\n", - "a1.text(5,140,'Variance = {0:.2f}'.format(r2_score(y_test, y_pred_test)), fontsize = 12)\n", - "a1.set_xlabel('Test samples', fontsize = 12)\n", - "a1.set_yticklabels([])\n", - "# plot histogram\n", - "a1.hist(y_residual_test, orientation = 'horizontal', color = 'b', bins = 10, histtype = 'step');\n", - "a1.hist(y_residual_test, orientation = 'horizontal', color = 'b', alpha = 0.2, bins = 10);\n", - "\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python [default]", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.6" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/automl/README.md b/automl/README.md index 7ad8581c2..e8e93f8c7 100644 --- a/automl/README.md +++ b/automl/README.md @@ -138,7 +138,7 @@ cd to the "automl" folder where the sample notebooks were extracted and then run |-|-|-| |**primary_metric**|This is the metric that you want to optimize.

Classification supports the following primary metrics
accuracy
AUC_weighted
balanced_accuracy
average_precision_score_weighted
precision_score_weighted

Regression supports the following primary metrics
spearman_correlation
normalized_root_mean_squared_error
r2_score
normalized_mean_absolute_error
normalized_root_mean_squared_log_error| Classification: accuracy

Regression: spearman_correlation |**max_time_sec**|Time limit in seconds for each iterations|None| -|**iterations**|Number of iterations. In each iteration trains the data with a specific pipeline|25| +|**iterations**|Number of iterations. In each iteration trains the data with a specific pipeline. To get the best result, use at least 100. |25| |**n_cross_validations**|Number of cross validation splits|None| |**validation_size**|Size of validation set as percentage of all training samples|None| |**concurrent_iterations**|Max number of iterations that would be executed in parallel|1| @@ -186,7 +186,7 @@ The main code of the file must be indented so that it is under this condition. # Troubleshooting ## Iterations fail and the log contains "MemoryError" This can be caused by insufficient memory on the DSVM. AutoML loads all training data into memory. So, the available memory should be more than the training data size. -If you are using a remote DSVM, memory is needed for each concurrent iteration. The concurrent_iterations setting specifies the maximum concurrent iterations. For example, if the trinaing data size is 8Gb and concurrent_iterations is set to 10, the minimum memory required is at least 80Gb. +If you are using a remote DSVM, memory is needed for each concurrent iteration. The concurrent_iterations setting specifies the maximum concurrent iterations. For example, if the training data size is 8Gb and concurrent_iterations is set to 10, the minimum memory required is at least 80Gb. To resolve this issue, allocate a DSVM with more memory or reduce the value specified for concurrent_iterations. ## Iterations show as "Not Responding" in the RunDetails widget. diff --git a/automl/automl_setup.cmd b/automl/automl_setup.cmd index 6d82a9072..201a06fe6 100644 --- a/automl/automl_setup.cmd +++ b/automl/automl_setup.cmd @@ -6,11 +6,10 @@ IF "%conda_env_name%"=="" SET conda_env_name="azure_automl" call conda activate %conda_env_name% 2>nul: if not errorlevel 1 ( - call conda env update -f automl_env.yml -n %conda_env_name% + call conda env update --file automl_env.yml -n %conda_env_name% if errorlevel 1 goto ErrorExit ) else ( call conda env create -f automl_env.yml -n %conda_env_name% - if errorlevel 1 goto ErrorExit ) call conda activate %conda_env_name% 2>nul: diff --git a/automl/automl_setup_linux.sh b/automl/automl_setup_linux.sh index 288e09cb7..6e0300549 100644 --- a/automl/automl_setup_linux.sh +++ b/automl/automl_setup_linux.sh @@ -9,7 +9,7 @@ fi if source activate $CONDA_ENV_NAME 2> /dev/null then - conda env update -f automl_env.yml -n $CONDA_ENV_NAME + conda env update -file automl_env.yml -n $CONDA_ENV_NAME else conda env create -f automl_env.yml -n $CONDA_ENV_NAME && source activate $CONDA_ENV_NAME && diff --git a/automl/automl_setup_mac.sh b/automl/automl_setup_mac.sh index 6d0049020..789f143fa 100644 --- a/automl/automl_setup_mac.sh +++ b/automl/automl_setup_mac.sh @@ -9,7 +9,7 @@ fi if source activate $CONDA_ENV_NAME 2> /dev/null then - conda env update -f automl_env.yml -n $CONDA_ENV_NAME + conda env update -file automl_env.yml -n $CONDA_ENV_NAME else conda env create -f automl_env.yml -n $CONDA_ENV_NAME && source activate $CONDA_ENV_NAME && diff --git a/onnx/onnx-inference-emotion-recognition.ipynb b/onnx/onnx-inference-emotion-recognition.ipynb new file mode 100644 index 000000000..e7b039b5d --- /dev/null +++ b/onnx/onnx-inference-emotion-recognition.ipynb @@ -0,0 +1,729 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Copyright (c) Microsoft Corporation. All rights reserved. \n", + "Licensed under the MIT License." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 02. Facial Expression Recognition using ONNX Runtime GPU on AzureML\n", + "\n", + "This example shows how to deploy an image classification neural network using the Facial Expression Recognition ([FER](https://www.kaggle.com/c/challenges-in-representation-learning-facial-expression-recognition-challenge/data)) dataset and Open Neural Network eXchange format ([ONNX](http://aka.ms/onnxdocarticle)) on the Azure Machine Learning platform. This tutorial will show you how to deploy a FER+ model from the [ONNX model zoo](https://github.com/onnx/models), use it to make predictions using ONNX Runtime Inference, and deploy it as a web service in Azure.\n", + "\n", + "Throughout this tutorial, we will be referring to ONNX, a neural network exchange format used to represent deep learning models. With ONNX, AI developers can more easily move models between state-of-the-art tools (CNTK, PyTorch, Caffe, MXNet, TensorFlow) and choose the combination that is best for them. ONNX is developed and supported by a community of partners including Microsoft AI, Facebook, and Amazon. For more information, explore the [ONNX website](http://onnx.ai) and [open source files](https://github.com/onnx).\n", + "\n", + "[ONNX Runtime](https://aka.ms/onnxruntime) is the runtime engine that enables evaluation of trained machine learning (Traditional ML and Deep Learning) models with high performance and low resource utilization.\n", + "\n", + "#### Tutorial Objectives:\n", + "\n", + "1. Describe the FER+ dataset and pretrained Convolutional Neural Net ONNX model for Emotion Recognition, stored in the ONNX model zoo.\n", + "2. Deploy and run the pretrained FER+ ONNX model on an Azure Machine Learning instance\n", + "3. Predict labels for test set data points in the cloud using ONNX Runtime and Azure ML" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Prerequisites\n", + "\n", + "### 1. Install Azure ML SDK and create a new workspace\n", + "Please follow [00.configuration.ipynb](https://github.com/Azure/MachineLearningNotebooks/blob/master/00.configuration.ipynb) notebook.\n", + "\n", + "\n", + "### 2. Install additional packages needed for this Notebook\n", + "You need to install the popular plotting library `matplotlib` and the `onnx` library in the conda environment where Azure Maching Learning SDK is installed.\n", + "\n", + "```sh\n", + "(myenv) $ pip install matplotlib onnx\n", + "```\n", + "\n", + "### 3. Download sample data and pre-trained ONNX model from ONNX Model Zoo.\n", + "\n", + "[Download the ONNX Emotion FER+ model and corresponding test data](https://www.cntk.ai/OnnxModels/emotion_ferplus/opset_7/emotion_ferplus.tar.gz) and place them in the same folder as this tutorial notebook. You can unzip the file through the following line of code.\n", + "\n", + "```sh\n", + "(myenv) $ tar xvzf emotion_ferplus.tar.gz\n", + "```\n", + "\n", + "More information can be found about the ONNX FER+ model on [github](https://github.com/onnx/models/tree/master/emotion_ferplus). For more information about the FER+ dataset, please visit Microsoft Researcher Emad Barsoum's [FER+ source data repository](https://github.com/ebarsoum/FERPlus)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Load Azure ML workspace\n", + "\n", + "We begin by instantiating a workspace object from the existing workspace created earlier in the configuration notebook." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Check core SDK version number\n", + "import azureml.core\n", + "\n", + "print(\"SDK version:\", azureml.core.VERSION)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from azureml.core import Workspace\n", + "\n", + "ws = Workspace.from_config()\n", + "print(ws.name, ws.location, ws.resource_group, ws.location, sep = '\\n')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Registering your model with Azure ML" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "model_dir = \"emotion_ferplus\" # replace this with the location of your model files\n", + "\n", + "# leave as is if it's in the same folder as this notebook" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from azureml.core.model import Model\n", + "\n", + "model = Model.register(model_path = model_dir + \"/\" + \"model.onnx\",\n", + " model_name = \"onnx_emotion\",\n", + " tags = {\"onnx\": \"demo\"},\n", + " description = \"FER+ emotion recognition CNN from ONNX Model Zoo\",\n", + " workspace = ws)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Optional: Displaying your registered models\n", + "\n", + "This step is not required, so feel free to skip it." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "models = ws.models()\n", + "for m in models:\n", + " print(\"Name:\", m.name,\"\\tVersion:\", m.version, \"\\tDescription:\", m.description, m.tags)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### ONNX FER+ Model Methodology\n", + "\n", + "The image classification model we are using is pre-trained using Microsoft's deep learning cognitive toolkit, [CNTK](https://github.com/Microsoft/CNTK), from the [ONNX model zoo](http://github.com/onnx/models). The model zoo has many other models that can be deployed on cloud providers like AzureML without any additional training. To ensure that our cloud deployed model works, we use testing data from the famous FER+ data set, provided as part of the [trained Emotion Recognition model](https://github.com/onnx/models/tree/master/emotion_ferplus) in the ONNX model zoo.\n", + "\n", + "The original Facial Emotion Recognition (FER) Dataset was released in 2013, but some of the labels are not entirely appropriate for the expression. In the FER+ Dataset, each photo was evaluated by at least 10 croud sourced reviewers, creating a better basis for ground truth. \n", + "\n", + "You can see the difference of label quality in the sample model input below. The FER labels are the first word below each image, and the FER+ labels are the second word below each image.\n", + "\n", + "![](https://raw.githubusercontent.com/Microsoft/FERPlus/master/FER+vsFER.png)\n", + "\n", + "***Input: Photos of cropped faces from FER+ Dataset***\n", + "\n", + "***Task: Classify each facial image into its appropriate emotions in the emotion table***\n", + "\n", + "``` emotion_table = {'neutral':0, 'happiness':1, 'surprise':2, 'sadness':3, 'anger':4, 'disgust':5, 'fear':6, 'contempt':7} ```\n", + "\n", + "***Output: Emotion prediction for input image***\n", + "\n", + "\n", + "Remember, once the application is deployed in Azure ML, you can use your own images as input for the model to classify." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# for images and plots in this notebook\n", + "import matplotlib.pyplot as plt \n", + "from IPython.display import Image\n", + "\n", + "# display images inline\n", + "%matplotlib inline" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Model Description\n", + "\n", + "The FER+ model from the ONNX Model Zoo is summarized by the graphic below. You can see the entire workflow of our pre-trained model in the following image from Barsoum et. al's paper [\"Training Deep Networks for Facial Expression Recognition\n", + "with Crowd-Sourced Label Distribution\"](https://arxiv.org/pdf/1608.01041.pdf), with our (64,64) input images and our output probabilities for each of the labels." + ] + }, + { + "attachments": { + "image.png": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAACc0AAAKYCAYAAACrLUKBAAAgAElEQVR4AezdB5QU55kv/IfMkOOQYcg5CRAgIZBQlhwkS7ItW/J61979rjdcbzx797tnb9gb9vq7G7zB67T2yl5LshLKESGBJDJIRIHIOeec+c77YvAwzICQBmmgf3XOQHd1dXXV763u6Z769/NUO3ny5MkwESBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBAhCoXgD7aBcJECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEAWEJpzIBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAwQgIzRXMUNtRAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEBCacwwQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQMEICM0VzFDbUQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAQmnMMECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEDBCAjNFcxQ21ECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQEJpzDBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAwQgIzRXMUNtRAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEBCacwwQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQMEICM0VzFDbUQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAQmnMMECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEDBCAjNFcxQ21ECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQEJpzDBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAwQgIzRXMUNtRAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEBCacwwQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQMEICM0VzFDbUQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAQmnMMECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEDBCAjNFcxQ21ECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQEJpzDBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAwQgIzRXMUNtRAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEBCacwwQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQMEICM0VzFDbUQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAQmnMMECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEDBCAjNFcxQ21ECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQEJpzDBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAwQgIzRXMUNtRAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEBCacwwQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQMEICM0VzFDbUQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAQmnMMECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEDBCAjNFcxQ21ECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQEJpzDBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAwQgIzRXMUNtRAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEBCacwwQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQMEICM0VzFDbUQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAQmnMMECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEDBCAjNFcxQ21ECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQEJpzDBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAwQgIzRXMUNtRAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEBCacwwQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQMEICM0VzFDbUQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAQmnMMECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEDBCAjNFcxQ21ECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQEJpzDBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAwQgIzRXMUNtRAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEBCacwwQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQMEICM0VzFDbUQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAQmnMMECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEDBCAjNFcxQ21ECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQEJpzDBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAwQgIzRXMUNtRAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEBCacwwQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQMEICM0VzFDbUQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAQmnMMECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEDBCAjNFcxQ21ECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQEJpzDBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAwQgIzRXMUNtRAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEBCacwwQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQMEICM0VzFDbUQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAQmnMMECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEDBCAjNFcxQ21ECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQEJpzDBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAwQgIzRXMUNtRAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEBCacwwQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQMEICM0VzFDbUQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAQmnMMECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEDBCAjNFcxQ21ECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQEJpzDBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAwQgIzRXMUNtRAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEBCacwwQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQMEICM0VzFDbUQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAQmnMMECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEDBCAjNFcxQ21ECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQEJpzDBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAwQgIzRXMUNtRAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEBCacwwQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQMEICM0VzFDbUQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAQmnMMECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEDBCAjNFcxQ21ECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQEJpzDBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAwQgIzRXMUNtRAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEBCacwwQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQMEICM0VzFDbUQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAQmnMMECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEDBCAjNFcxQ21ECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQEJpzDBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAwQgIzRXMUNtRAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEBCacwwQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQMEICM0VzFDbUQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAQmnMMECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEDBCAjNFcxQ21ECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQEJpzDBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAwQgIzRXMUNtRAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEBCacwwQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQMEICM0VzFDbUQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAQmnMMECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEDBCAjNFcxQ21ECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQEJpzDBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAwQgIzRXMUNtRAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEBCacwwQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQMEICM0VzFDbUQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAQmnMMECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEDBCAjNFcxQ21ECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQEJpzDBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAwQgIzRXMUNtRAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEBCacwwQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQMEICM0VzFDbUQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAQmnMMECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEDBCAjNFcxQ21ECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQEJpzDBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAwQgIzRXMUNtRAgQIECBAgAABAgQIECBAgAABAgQIECBA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QIAAAQIECBAgUFUFhOaq6sjYLgIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBCodAGhuUontUICBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQqKoCQnNVdWRsFwECBAgQIECAAAECBAgQINQqvaIAACAASURBVECAAAECBAgQIECAAAECBAgQIECAAAECBAhUuoDQXKWTWiEBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIVFUBobmqOjK2iwABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQqXUBortJJrZAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEqqqA0FxVHRnbRYAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQKVLiA0V+mkVkiAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECVVVAaK6qjoztIkCAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIFKFxCaq3RSKyRAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBqiogNFdVR8Z2ESBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEClCwjNVTqpFRIgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAVRUQmquqI2O7CBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQKDSBYTmKp3UCgkQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECgqgoIzVXVkbFdBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFDpAkJzlU5qhQQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQVQWE5qrqyNguAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEKh0AaG5Sie1QgIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIE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wK03XVOEEzOs8cgvXyh+PWRg44WXXouIfJz6yPZ/Awf0i2uuujS+cOdH4pabrj71BT4lEFGEqS6ccn7xjxAyhPm9f7g36vv2iZs/eEWMGDE06tpR6SxDWhksyn/o8MzzC+LRJ+YWLZ8zPHvtVZdybodA/mORCy84P0aNGBKr126M//1/7i/+cchHbrkuxowcGu35Bwm5DwcOHopt23bGsy++uw9ZsTlbos+59vKismU7ptLtT8m/k+R+5N8zli5fU4TK88+3s6k4lxVAl7+z9phtv759zqqK4LGBvCBAoEMEGhoaonfv3sf+sUiH3MSgBAgQIECAQEUJCM1V1HaZLAECBAgQIECAAAECBAgQOHuBrEJ10YUTisfZj2YEAicW+MjN18bl0y+IF19+rWgN+uOf/jIOHTp84pNbvdurV2187COz47987tfiA5ddENni0nHmAh+99fpjrQDv/vHDxUDbGnfGk8+8UjzyjaImzn8WxmluPvG9zh8/On73t++Mr9z1sXaFPE48Svd9NwOMWWnxjttvjHvvfyx+fN+jsX1HU7tAbrrhivgvn72tCCm1rkjXroud1G0F+vSui5mXXxi33nR1/MM/3x9Ll62OP/rv/18R8PnyXR+LC6aMb9Mmq9Eufvud+PF9v4y//n/vLoJbedHE88fEzTde2eb1ToiiImRW/P3YR66Pv/n+vxX+//27/zuWrVgbv/mVTxbh5bacDhw4FEtXrIl7//2xYh92Nu0pLhk7ZkTc9uFro7paZbO2DPPzDORffMGE4g+9ufNei9feXBZXXzk9zub31SXLVhYB85b7jx49rGij3fK1ZwIEykvgtttuK68JmQ0BAgQIECBwzgWE5s75FpgAAQIECBAgQIAAAQIECBDoXIGWNoyde1d3624C+X02eHBD3HD9zLhs+gXxe//1i/H2slWxctWG2LZtR+w/+F6Fw7petcUPrcePHRVTJo2NhoZ+xQ+de/euUwniLL9xch+yHet/+73fiFs/dHX88smXimpRGzZuPTZykZM7SVguq8plKCP38cKp44sKOn4POUbX7hdpNqB/v7hq5rSiLes3vv7pWP7Ounhn5bqiite+/QePjdWrtqb4/h87dkRMnXx+DGroFwMHDoj6vn49HEPyok2B/J6bdtHE+OLnfi02b9kejz7xYtGe+R9/+LN49PG5xe+1V105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BAgQIECAAAECBAgQIFDJAjfccENUVVVFjx49KnkZ5k6AAAECBAiUUEBoroSYhiJAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACB8hLo2bNneU3IbAgQIECAAIFzLiA0d863wAQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAoKMEli5dGtXV1UWL1oaGho66jXEJECBAgACBChIQmqugzTJVAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEDg9gbVr10ZNTU306tUrhOZOz87ZBAgQIECgqwoIzXXVnbUuAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEIj6+vro0aNH1NbW0iBAgAABAgQIFAJCc74RCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQKDLCsyaNavLrs3CCBAgQIAAgTMTqD6zy1xFgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQqT0ClucrbMzMmQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgXYKvPnmm0V71qFDh8agQYPaeZXTCBAgQIAAga4sIDTXlXfX2ggQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQINDNBV5++eXo2bNnXHrppUJz3fx7wfIJECBAgECLgNBci4RnAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEOhyAitWrIi6uro4//zzu9zaLIgAAQIECBA4MwGhuTNzcxUBAgQIECBAoMsL7Nm7L7Zs3d7l11mqBaaXgwABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACB8hcQmiv/PTJDAgQIECBAgMA5Efje3/8kfnjPQ+fk3pV406bdeypx2uZMgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAoNsJCM11uy23YAIECBAgQIBA+wSadu+NfDg6V2D5O2vjtl//VufetILvtqtJWPFstu+nP3si3nr7nbMZoltd+86q9d1qvRZLgAABAgQIECBAgAABAgS6isC0adOiZ8+eMXjw4K6yJOsgQIAAAQIEzlJAaO4sAV1OgAABAgQIEOhKAnV1dfH7v//7sXPnzq60rE5dS0NDQ/E/4M70phlUfOjR58/0ctd1ksCSZzfElpVN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bFjU1taeck75+cCBA095ztl8ePvtt8fYsWNPe4hDhw7F3XffHVdffXVMmjQpsmV1e45rr722Pac5hwABAgQIECBQEQJVzVmD1kGAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECJxV44YUX4rd+67fi7//+72PatGnRp0+fk55bzh/s3bs3brvttvjWt75VPNfV1ZXzdEsyt3vuuSe2bNkSl156aWTg00GAAAECBAgQUGnO9wABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgTaEOjRo0f069cv8rkrHE1NTbFv377oDqG5iy66KDIsOHz48K6wddZAgAABAgQIlEBAaK4EiIYgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQKBrC2Tg6lOf+lTRmrWmxo9ZK2m3MzSXR1VVVSVN21wJECBAgACBDhTwt7kOxDU0AQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQJdQyBDc3feeWcMHjw4Kjk0l5Xybrnllpg4cWL07Nmza2xOG6uo5P1qY2k+JkCAAAECBM5QoKq5ubn5DK91GQECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAhUkMCRI0diyZIlMXTo0Bg4cGBFBwDby7569eo4cOBADBgwoKgU2N7rnEeAAAECBAh0XQGV5rru3loZAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIlEti3b19s2rQp+vbtW4SvKrVKW1aaa2lXWiKash9m8+bNsXv37mKew4YNK/v5miABAgQIECDQ8QLVHX8LdyBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEBlCzQ1NcX8+fNj5cqVkQG6Sj2OHj0a27dvjz179sThw4crdRmnNe8+ffpEfX199OrV67SuczIBAgQIECDQdQVUmuu6e2tlBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAiUSKCqqipqa2sjnyv5yPasjz/+eIwdOzYmTZpUtGit5PW0Z+7drbJee0ycQ4AAAQIEuruA0Fx3/w6wfgIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIE2hQYOHBgzJkzpwjOVWpr1jYX6QQCBAgQIECAQDcRqGpubm7uJmu1TAIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECHRrgWzPunbt2ujbt2+3aVma621sbIyGhoaiwl63/gaweAIECBAgQKAQUGnONwIBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgTaENi3b19s2rSpaM86dOjQ6NOnTxtXlOfH1dXV3S44tmrVqnjzzTdjwoQJ3W7t5fldaFYECBAgQODcCwjNnfs9MAMCBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBMpcYNeuXTFv3rzI0NnVV19dsaG5bES2Y8eOyIpzvXv3rth1nM63y7Zt22L58uVFdb3Tuc65BAgQIECAQNcVEJrruntrZQQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIlEhg79698cYbbxSjTZs2rUSjdv4wR44ciYULF8b+/ftj4sSJMXny5M6fhDsSIECAAAECBM6xgNDcOd4AtydAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEBnCWSFuWxVun379qLSnNBcZ8m7DwECBAgQIFBOAtXlNBlzIUCAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQDkK1NTURP/+/YvWpgcPHizHKbZ7TocPH45Dhw5FVp3rDseYMWNixowZMW7cuO6wXGskQIAAAQIE2iGg0lw7kJxCgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgED3Fujdu3dMmjQpevToEX369OneGBW2+gzLDRo0yL5V2L6ZLgECBAgQ6EgBobmO1DU2AQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQJdQqBv375xySWXRFYtGzx4cMWuqaqqqph/Vs7LNXWHI/erkvesO+yRNRIgQIAAgc4WqGpubm7u7Ju6HwECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAh0vkC2ZH3jjTdi//79MWrUqCIE2Pmz6Nw7HjhwoGhHm0HBurq6zr25uxEgQIAAAQJlKSA0V5bbYlIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAovUBLTZV8zqpz+ejqx5IlS2Lt2rUxdOjQmD59eldfrvURIECAAAEC7RCobsc5TiFAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEC3FsjKbKtXr46HH344Nm7cWLEWLUG56urqbhGYa9molrBgy9eeCRAgQIAAge4tUNO9l2/1BAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQaFsgQ1fZ5nP79u1Fq8+2ryjPM44ePRqNjY1F5bWBAwfGuHHjynOiJZzV8OHDo76+Pnr37l3CUQ1FgAABAgQIVLKA0Fwl7565EyBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECDQKQI9evSIAQMGxJQpU4oAVqfctINusnfv3lizZk1kELA7hOYaGhoiHw4CBAgQIECAQIuA0FyLhGcCBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAicRKBnz54xbNiw4nGSUyrm7bq6uhg8eHD07du3YuZ8NhPNkGBWCaytra34wOPZOLiWAAECBAgQeE+gqlnz9vc0vCJAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgMAJBPLHqvk4dOhQ1NTURFaec1SGwJtvvhmrVq2KESNGxIwZMypj0mZJgAABAgQIdKhAdYeObnACBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAh0AYHDhw/Htm3b4t57741169Z1gRV1nyWsXLky5s6dG8uWLes+i7ZSAgQIECBA4JQCQnOn5PEhAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEIo4cORKNjY1x3333VXRoLqvlNTU1xfPPPx+LFy/uFlubgcf9+/cXVQK7xYItkgABAgQIEGhTQGiuTSInECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECDQ3QWyHWvPnj3jjTfeiJ07d1YsR4bm9uzZE0888UQsWrSoYtdh4gQIECBAgACBsxEQmjsbPdcSIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQINAtBGpra6OhoSFGjBgRvXr1qvg1Z7W5vXv3Vvw62rOA+vr6GDp0aPTv3789pzuHAAECBAgQ6AYCNd1gjZZIgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBsxaoq6uLL3zhCzFu3LizHutcDVBVVRV9+/aNG2+8MQYPHnyuptGp9x0/fnwRdOwu6+1UXDcjQIAAAQIVKlDVnPV3HQQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBwSoH80eqhQ4eipqYmqqsrs6lXy4+Hjxw5Ehmgy7azXf04evRo5KO7rLer76f1ESBAgACBUggIzZVC0RgECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUBEC2rNWxDaZJAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAEC51ogq8y99NJL8fLLL8e2bduK6fTu3TtmzZpVPAYMGBBbt26NdevWFZXopkyZUrQFzXNXr14dGzZsiGzx2qtXrxg0aFAMHz68eM7qbzt37oys/pafZ/tUR+kEduzYEevXr48+ffpEtmp1ECBAgAABAgSE5nwPECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAoJ0C2eYzw3NNTU3Fcwbc9u3bV7T/zCH2798fW7ZsKdqeTpgwoQjI5bnLly+P11577Vhr10mTJsWMGTOOhebeeeedYpxRo0YVobmDBw/G4sWLi8euXbuK2WVA78ILL4yLLrqoCIA1NjYeu9e4ceOitrY2tm/fXgTEMryXX+dj4MCBMWTIkGhoaCj8yAwEAAAgAElEQVTm2RLQyxBZPrr6sXfv3iK0mA5Cc119t62PAAECBAi0T0Born1OziJAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAoJsLZADt+uuvj2uuuSZ2794de/bsKSrKtQ6f1dfXx9ixY6OqqqoIrCVZz549i+BaBuIywJWPrCqXAbyWI4NsOWZNTU3kGBmQW7RoUdx3331FCC7Py9DXJz/5ySL4lffMYNzrr79e3GfkyJHF8+bNm2Pu3LmxcOHCIrCXVe0yaDdz5sxjoblVq1bFgQMHIufTHUJzuW9ZBVAFv5bvNs8ECBAgQIBAVXPW+nUQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAwDkVyB/dPvPMM7FixYr48pe/3OZcMmiXleWqq6uLAFwG7rIF7Ntvv12MkSG8rHKXFe8+8IEPxOTJk+Pw4cPx5JNPFsG9bB97wQUXtHkfJxAgQIAAAQIEupqA0FxX21HrIUCAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECgIgWy+tzf/M3fxPPPPx/33HNPm2vI8/ORVe0yMJfPGYrLKnLZQjYr2eUjK61l5bqseJfBvKx0l+/n11mJzkGAAAECBAgQ6G4C2rN2tx23XgIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEylKgpeVreyfXo0ePyEfrI8Nz+TjZkcG67tamtLGxMVavXl2sO6vtOQgQIECAAAEC1QgIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBA4NwLHDx4sJiE6m+l3YstW7YU1ftef/310g5sNAIECBAgQKBiBU7+TwwqdkkmToAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgcoTqK6ujoEDB8bw4cMrb/JlPOOmpqZYunRp0ZK2jKdpagQIECBAgEAnCgjNdSK2WxEgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQOBkAr17944ZM2bEuHHjTnaK9wkQIECAAAECBEogIDRXAkRDECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBA4GwFMjR3+eWXq4h2tpCuJ0CAAAECBAi0ISA01waQjwkQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQINAZAlVVVVFbW9sZt+pW9xg8eHDMnDkzhg4d2q3WbbEECBAgQIDAyQWqmpubm0/+sU8IECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAoDMEGhsbY/v27dGjR48YP358Z9yyW9xj9+7dsWXLlqirq4uRI0d2izVbJAECBAgQIHBqAZXmTu3jUwIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECHSKwK5du2Lt2rVFtTmhudKR19fXRz4cBAgQIECAAIEWAaG5FgnPBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQOIcC2Zq1b9++UVPjx7jncBvcmgABAgQIEOgGAtqzdoNNtkQCBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBMpfoLm5+dgkq6qqjr32ggABAgQIECBAoLQC/olCaT2NRoAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgTMSEJQ7IzYXESBAgAABAgROW0Bo7rTJXECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIHSC6xbty6ampqiT58+MXbs2NLfwIgECBAgQIAAAQKFQDUHAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEDj3AitWrIh58+bFsmXLzv1kzIAAAQIECBAg0IUFVJrrwptraQQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIVI7AmjVrisBcXV1d5UzaTAkQIECAAAECFSig0lwFbpopEyBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECDQ9QR27doVzc3NMWDAgK63OCsiQIAAAQIECJSRgNBcGW2GqRAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAxwpoz9qxvkYnQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAuwQmTZoUgwYNihEjRrTrfCcRIECAAAECBAicmYDQ3Jm5uYoAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIlFbjooovi4MGD2rOWVNVgBAgQIECAAIFfFahqbm5u/tW3vUOAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBLqegEpzXW9PrYgAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgQoU2LNnT1RVVUVdXV1UV1dX4ApMmQABAgQIECBQGQL+plUZ+2SWBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAh0cYFnn302Xnzxxdi7d28XX6nlESBAgAABAgTOrYDQ3Ln1d3cCBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgUAkePHo18OAgQIECAAAECBDpWQHvWjvU1OgECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBNolcPHFFxdtWXv16tWu853UfoFNmzbF4sWL4/XXX48NGzbEjh074siRI1FbWxuDBw+OyZMnx8yZM2Ps2LHRu3fv9g/sTAIECBAgQKAiBaqam5ubK3LmJk2AAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBNoQmD9/fjzzzDPxwgsvvC80l1X9ampq3heamzNnTlxxxRUxatSoNkb1MQECBAgQIFDJAkJzlbx75k6AAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECJxQ4dOhQbN68Ob7zne/E/fffX7weNGhQDB8+PHr27BlVVVWxf//+aGxsjC1btsS+ffti9uzZ8e1vfztuueUWFedOqOpNAgQIECDQNQS0Z+0a+2gVBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAh0sEA28crH4cOHi9ae+bq6ujqynWoGsNp7ZFvQlkfrxmA5Vj569OhRPE41Xl6XldLykWPlc8uRc8kxWsY6nbm1jNEy/sGDB4s15/s5XlZmy7HPZMyWsTvrefv27fHP//zP8ZOf/CQ2btwYU6dOjU9/+tPx1a9+NUaPHl2s5e23346f//zn8cMf/jAWLlwYTz/9dFx++eVx4YUXFud31lzdhwABAgQIEOhcAaG5zvV2NwIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEKlQgw2krV66Mf/3Xf43HH388NmzYEBdccEHcfffdUVdX1+5VzZ07N5544ol4/vnnY+nSpceuy1DXtddeGx/84AfjyiuvPPb+iV7s3bu3uDbHePHFF+PVV1+NAwcOFKeOGDEirrvuurjqqquK56yudrrH7t27Y968efGXf/mX8c477xRBwZzft771raIaWwYFy/3YtWtX3HvvvZHhuWy3+qlPfaqY/4ABA46FEsePHx+f+cxnYuTIkfGVr3ylMHzjjTciH7leBwECBAgQINA1BYTmuua+WhUBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAiUSyDDa+vXr48knnyzafGagKgNzGVzLimutq8Wd7JYtrUIzYJeBuWXLlsWmTZsig10tR7YSzcpnTz31VFxzzTXxta99LYYMGRK1tbUtpxTPCxYsiP/4j/8oQndr164t2o5u27btWLW5fG/NmjXx2GOPxe233x533HFHUTntfYOc4ousLrd8+fL48z//83jppZeKOeYas8Lc6tWriwBdJYTmct8ylJjPGYC77LLLCs/WS882rRkynD59ekycOLFYd7ZqzYeDAAECBAgQ6LoCQnNdd2+tjAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBsxTIYFtWccug27PPPhsvv/xyEZLLdqgZJMtWre05sj3o/fffX7QLzcBc3759iyp1EyZMKEJxe/bsKQJzq1atinxkdbehQ4cWobesktb6uO++++Lf/u3fimDc4MGDY9asWTFw4MAi1JbzzeBdXp9V8Zqamop75f3Gjh3bepiTvs65PvPMM8WjX79+RfvZ/fv3F6G8fK6UI/cnXfM5KwGeLOiXLWf79OlzrOVse9rjVoqBeRIgQIAAAQInFhCaO7GLdwkQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQ6OYCO3fuLMJyP/7xj4uqbslx6aWXFpXXMliWn7fn2LdvX7z++utFYG7x4sVFtbPrr78+brnllpgxY0YR6NqxY0cRynvooYeKYN6KFSviBz/4QRGsy2Bc68DX/Pnzi9tmNbqZM2fGnDlzivaiGfbKsFwG/H7+858XVdbyfg8++GARmGtPaC7n+tprrxWV7DIg9+EPfzgWLVpUBPDas9ZyOic9slLf1q1bi0qBWSUvQ3QZIGx95Hv5We5pVgTMkOLxQcXW53tNgAABAgQIVL6A0Fzl76EVECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECDQAQIZXPvRj35UtGUdOXJkXHfddfG5z32uqBj3wAMPtDs0t27dupg7d27MmzevmOWHPvSh+MY3vhHXXnvt+2adgbysLpcBuoULFxbXZCvWyZMnx+jRo4+dm21GM3SX42RorvVx8cUXx7Rp04oQ3R/+4R8WIbCsjvfKK68UbVpbn3ui1znXp59+Oh5//PHo379/3HXXXZHhuaxaV2lHhuMyUPjII4/EW2+9VYQSP/CBDxRtWrMta7bWzVa0Wfkv291mCDLDidmq9aKLLqq05ZovAQIECBAgcBoC1adxrlMJECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECDQrQQmTpwYv/mbvxn/8A//EH/1V39VhNUyTHY6x/Lly4vQWss1n/jEJ4pgVsvXLc8Z8srqcR/96Edb3iquOz6w9gd/8Afxu7/7u0XVu2Mntnpx3nnnFVXsstJaHhnC2759e6szTv7yySefjEcffbRo9/rpT386rrzyyuL1ya8o30+yZe3Xvva1IoiYwb+svvcnf/InReW8AwcOFBNfunRp3H333fG9732vaLV74403xg033BBp6CBAgAABAgS6roBKc113b62MAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIGzEJg0aVJREa66ujqGDRsW9fX1xWj59ekcWcksW6pmFbNZs2bF+eefH3369PmVIbLy2bhx4+Lyyy8vWohm29AMzGV70dbH8e1FW3+WrzMsl8G+rKqWlepynObm5uNP+5Wvn3vuuSIwlxX2Miz427/920Xg7HTX+ysDn6M30ji9v/Wtb8W//Mu/FNX7XnjhhfjmN78ZH/zgB4s9yFa0L774YmF25513xte//vXCrSVweI6m7rYECBAgQIBABwsIzXUwsOEJ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sggQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQKA+BrCp39dVXx49//ONYu3ZtPPDAA0VALsNvGerKcNeKFSvipz/9adFqNSui9evXL772ta8VFepaKscdOXIkdu7cGX/xF39RVEzLanRZTe3rX/96EYhbtmzZSRecFdWyUtykSZNOek5X/SBDh7fffnvRdjWDh/lofWSgMB8ZXsyHgwABAgQIEOj6AkJzXX+PrZAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgTMQWL16dfzP//k/i4Ba68sz+LZt27birTxn9uzZx8JWGU77wAc+EH/913997JKsJpetUP/wD/8w/viP/7i4Ntuwrlu3Lu65554i8LZ///7Yvn17NDU1FW1b77jjjvjMZz4TI0aMKAJdOVhWo3vnnXdi5cqVxXn53po1a4oQXV1d3bHzjt241YusMJdz+Lu/+7tW73aPly1V9rLSnoMAAQIECBAgkAJCc74PCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgcAKBDLItXbo0XnrppRN8+u5bec7LL7987POsCldfX3/s63yRYa2sdnbbbbcVr59++unI0NyqVati/fr1xbkZest2rDfeeGNRfS6fx44dGxnCa31klbQMz2Wb1jzy/hmia+vI0FxLm9e2zj3+87w255FV2Fqq3h1/jq8JECBAgAABApUkIDRXSbtlrgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIdJrAgAEDiqDbmDFj2n3PmpqaomXq8Rdk8CzH+fznPx9Tp06NJUuWRFapa2xsLE7N0Fx+Pm3atLj44ouLanPHj5Fjjxw5Mu66667YtGnT8R+f8uuTzeuUF/3nhzfffHM0NDTEkCFD4pJLLvmVIF97xnAOAQIECBAgQKCcBKqaj2/YXk6zMxcCBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFBCgeoSjmUoAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQ1gJCc2W9PSZHgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAqUUEJorpaaxCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQKCsBYTmynp7TI4AAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIESikgNFdKTWMRIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQFkLCM2V9faYHAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAiUUkBorpSaxiJAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBshYQmivr7TE5AgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECilgNBcKTWNRYAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQJlLSA0V9bbY3IECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUEoBoblSahqLAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBMpaQGiurLfH5AgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECglAJCc6XUNBYBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgNDs0zwAACAASURBVAABAgQIlLWA0FxZb4/JESBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEApBYTmSqlpLAIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAoawGhubLeHpMjQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgVIKCM2VUtNYBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFDWAkJzZb09JkeAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECpRQQmiulprEIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAoKwFhObKentMjgABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgRKKSA0V0pNYxEgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAWQsIzZX19pgcAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECJRSQGiulJrGIkCAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIGyFhCaK+vtMTkCBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQKKWA0FwpNY1FgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAmUtIDRX1ttjcgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQSgGhuVJqGosAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEylpAaK6st8fkCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQKCUAkJzpdQ0FgECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAiUtYDQXFlvj8kRIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQCkFhOZKqWksAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEChrAaG5st4ekyNAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBUgoIzZVS01gECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUNYCQnNlvT0mR4AAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQKlFBCaK6WmsQgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECgrAWE5sp6e0yOAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBEopIDRXSk1jESBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEBZCwjNlfX2mBwBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIlFJAaK6UmsYiQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgbIWEJor6+0xOQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAopYDQXCk1jUWAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECZS0gNFfW22NyBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFBKAaG5UmoaiwABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgTKWkBorqy3x+QIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAoJQCQnOl1DQWAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECJS1gNBcWW+PyREgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAKQWE5kqpaSwCBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQKGsBobmy3h6TI0CAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIFSCgjNlVLTWAQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQ1gJCc2W9PSZHgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAqUUEJorpaaxCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQKCsBYTmynp7TI4AAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIESikgNFdKTWMRIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQFkLCM2V9faYHAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAiUUkBorpSaxiJAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBshYQmivr7TE5AgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECilgNBcKTWNRYAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQJlLSA0V9bbY3IECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUEoBoblSahqLAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBMpaQGiurLfH5AgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECglAJCc6XUNBYBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIlLWA0FxZb4/JESBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEApBYTmSqlpLAIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAoawGhubLeHpMjQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgVIKCM2VUtNYBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFDWAkJzZb09JkeAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECpRQQmiulprEIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAoKwFhObKentMjgABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgRKKSA0V0pNYxEgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAWQsIzZX19pgcAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECJRSQGiulJrGIkCAAAECBAgQIECAAAECBAgQIECAAAECBAgQIEDg/2fvPKCiuL4//p1tVAVERUTFir0gJhp7S1Ss2I0l9hY1TU352WOKsSTG3nuJJqjRqIm919hijwr6t2ADFBZYttz/mV12d2YbCy4rmMc5nJ3ZnXnl896779777rxhBBgBRoARYAQYAUaAEWAEGIFcTYAFzeXq5mGFYwQYAUaAEWAEGAFGgBFgBBgBRoARYAQYAUaAEWAEGAFGgBFgBBgBRoARYAQYAUaAEWAEGAFGgBFgBBgBRoARcCUBFjTnSposLUaAEWAEGAFGgBFgBBgBRoARYAQYAUaAEWAEGAFGgBFgBBgBRoARYAQYAUaAEWAEGAFGgBFgBBgBRoARYAQYgVxNgAXN5ermYYVj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IIAAAgggEM0CFM1Fc/ToOwIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAQEQCFM1FxEVjBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQACBaBagaC6ao0ffEUAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEIhKgaC4iLhojgAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAghEswBFc9EcPfqOAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCAQkQBFcxFx0RgBBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQCCaBSiai+bo0XcEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAIGIBCiai4iLxggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAtEsQNFcNEePviOAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCEQkQNFcRFw0RgABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQiGYBiuaiOXr0HQEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAICIBiuYi4qIxAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIBANAtQNBfN0aPvCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACEQlQNBcRF40RQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQSiWYCiuWiOHn1HAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBCISICiuYi4aIwAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIBDNAhTNRXP06DsCCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggEBEAhTNRcRFYwQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAgWgWoGgumqNH3xFAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBCISoGguIi4aI4AAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIRLMARXPRHD36jgACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgggEJEARXMRcdEYAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAgmgUomovm6NF3BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQACBiAQomouIi8YIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAALRLEDRXDRHj74jgAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAghEJEDRXERcNEYAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEIhmAYrmojl69B0BBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQCAiAYrmIuKiMQIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAQDQL/H9uzui24q8DRwAAAABJRU5ErkJggg==" + } + }, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![image.png](attachment:image.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Deploy our model on Azure ML" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We are now going to deploy our ONNX Model on AML with inference in ONNX Runtime. We begin by writing a score.py file, which will help us run the model in our Azure ML virtual machine (VM), and then specify our environment by writing a yml file.\n", + "\n", + "You will also notice that we import the onnxruntime library to do runtime inference on our ONNX models (passing in input and evaluating out model's predicted output). More information on the API and commands can be found in the [ONNX Runtime documentation](https://aka.ms/onnxruntime).\n", + "\n", + "### Write Score File\n", + "\n", + "A score file is what tells our Azure cloud service what to do. After initializing our model using azureml.core.model, we start an ONNX Runtime GPU inference session to evaluate the data passed in on our function calls." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile score.py\n", + "import json\n", + "import numpy as np\n", + "import onnxruntime\n", + "import sys\n", + "import os\n", + "from azureml.core.model import Model\n", + "import time\n", + "\n", + "def init():\n", + " global session\n", + " model = Model.get_model_path(model_name = 'onnx_emotion')\n", + " session = onnxruntime.InferenceSession(model, None)\n", + " \n", + "def run(input_data):\n", + " '''Purpose: evaluate test input in Azure Cloud using onnxruntime.\n", + " We will call the run function later from our Jupyter Notebook \n", + " so our azure service can evaluate our model input in the cloud. '''\n", + "\n", + " try:\n", + " # load in our data, convert to readable format\n", + " start = time.time()\n", + " data = np.array(json.loads(input_data)['data']).astype('float32')\n", + "\n", + " r = session.run([\"Plus214_Output_0\"], {\"Input3\": data})[0]\n", + " result = emotion_map(postprocess(r[0]))\n", + " end = time.time()\n", + " result_dict = {\"result\": np.array(result).tolist(),\n", + " \"time\": np.array(end - start).tolist()}\n", + " except Exception as e:\n", + " result_dict = {\"error\": str(e)}\n", + " \n", + " return json.dumps(result_dict)\n", + "\n", + "def emotion_map(classes, N=1):\n", + " \"\"\"Take the most probable labels (output of postprocess) and returns the top N emotional labels that fit the picture.\"\"\"\n", + " \n", + " emotion_table = {'neutral':0, 'happiness':1, 'surprise':2, 'sadness':3, 'anger':4, 'disgust':5, 'fear':6, 'contempt':7}\n", + " emotion_keys = list(emotion_table.keys())\n", + " emotions = []\n", + " for i in range(N):\n", + " emotions.append(emotion_keys[classes[i]])\n", + " return emotions\n", + "\n", + "def softmax(x):\n", + " \"\"\"Compute softmax values (probabilities from 0 to 1) for each possible label.\"\"\"\n", + " x = x.reshape(-1)\n", + " e_x = np.exp(x - np.max(x))\n", + " return e_x / e_x.sum(axis=0)\n", + "\n", + "def postprocess(scores):\n", + " \"\"\"This function takes the scores generated by the network and returns the class IDs in decreasing \n", + " order of probability.\"\"\"\n", + " prob = softmax(scores)\n", + " prob = np.squeeze(prob)\n", + " classes = np.argsort(prob)[::-1]\n", + " return classes" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Write Environment File" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from azureml.core.conda_dependencies import CondaDependencies \n", + "\n", + "myenv = CondaDependencies()\n", + "myenv.add_pip_package(\"numpy\")\n", + "myenv.add_pip_package(\"azureml-core\")\n", + "myenv.add_pip_package(\"onnxruntime-gpu\")\n", + "\n", + "\n", + "with open(\"myenv.yml\",\"w\") as f:\n", + " f.write(myenv.serialize_to_string())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Create the Container Image\n", + "\n", + "This step will likely take a few minutes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from azureml.core.image import ContainerImage\n", + "\n", + "# enable_gpu = True to install CUDA 9.1 and cuDNN 7.0\n", + "\n", + "image_config = ContainerImage.image_configuration(execution_script = \"score.py\",\n", + " runtime = \"python\",\n", + " conda_file = \"myenv.yml\",\n", + " description = \"test\",\n", + " tags = {\"demo\": \"onnx\"},\n", + " enable_gpu = True\n", + " )\n", + "\n", + "\n", + "image = ContainerImage.create(name = \"onnxtest\",\n", + " # this is the model object\n", + " models = [model],\n", + " image_config = image_config,\n", + " workspace = ws)\n", + "\n", + "image.wait_for_creation(show_output = True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Debugging\n", + "\n", + "In case you need to debug your code, the next line of code accesses the log file." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(image.image_build_log_uri)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We're all set! Let's get our model chugging.\n", + "\n", + "## Deploy the container image" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from azureml.core.webservice import AciWebservice\n", + "\n", + "aciconfig = AciWebservice.deploy_configuration(cpu_cores = 1, \n", + " memory_gb = 1, \n", + " tags = {'demo': 'onnx'}, \n", + " description = 'ONNX for facial emotion recognition model')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The following cell will likely take a few minutes to run as well." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from azureml.core.webservice import Webservice\n", + "\n", + "aci_service_name = 'onnx-emotion-demo'\n", + "print(\"Service\", aci_service_name)\n", + "\n", + "aci_service = Webservice.deploy_from_image(deployment_config = aciconfig,\n", + " image = image,\n", + " name = aci_service_name,\n", + " workspace = ws)\n", + "\n", + "aci_service.wait_for_deployment(True)\n", + "print(aci_service.state)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "if aci_service.state != 'Healthy':\n", + " # run this command for debugging.\n", + " print(aci_service.get_logs())\n", + "\n", + " # If your deployment fails, make sure to delete your aci_service before trying again!\n", + " # aci_service.delete()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Success!\n", + "\n", + "If you've made it this far, you've deployed a working VM with a facial emotion recognition model running in the cloud using Azure ML. Congratulations!\n", + "\n", + "Let's see how well our model deals with our test images." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Testing and Evaluation" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Useful Helper Functions\n", + "\n", + "We preprocess and postprocess our data (see score.py file) using the helper functions specified in the [ONNX FER+ Model page in the Model Zoo repository](https://github.com/onnx/models/tree/master/emotion_ferplus)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def preprocess(img):\n", + " \"\"\"Convert image to the write format to be passed into the model\"\"\"\n", + " input_shape = (1, 64, 64)\n", + " img = np.reshape(img, input_shape)\n", + " img = np.expand_dims(img, axis=0)\n", + " return img" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# to manipulate our arrays\n", + "import numpy as np \n", + "\n", + "# read in test data protobuf files included with the model\n", + "import onnx\n", + "from onnx import numpy_helper\n", + "\n", + "# to use parsers to read in our model/data\n", + "import json\n", + "import os\n", + "\n", + "test_inputs = []\n", + "test_outputs = []\n", + "\n", + "# read in 3 testing images from .pb files\n", + "test_data_size = 3\n", + "\n", + "for i in np.arange(test_data_size):\n", + " input_test_data = os.path.join(model_dir, 'test_data_set_{0}'.format(i), 'input_0.pb')\n", + " output_test_data = os.path.join(model_dir, 'test_data_set_{0}'.format(i), 'output_0.pb')\n", + " \n", + " # convert protobuf tensors to np arrays using the TensorProto reader from ONNX\n", + " tensor = onnx.TensorProto()\n", + " with open(input_test_data, 'rb') as f:\n", + " tensor.ParseFromString(f.read())\n", + " \n", + " input_data = preprocess(numpy_helper.to_array(tensor))\n", + " test_inputs.append(input_data)\n", + " \n", + " with open(output_test_data, 'rb') as f:\n", + " tensor.ParseFromString(f.read())\n", + " \n", + " output_data = numpy_helper.to_array(tensor)\n", + " test_outputs.append(output_data)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "nbpresent": { + "id": "c3f2f57c-7454-4d3e-b38d-b0946cf066ea" + } + }, + "source": [ + "### Show some sample images\n", + "We use `matplotlib` to plot 3 images from the dataset with their labels over them." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "nbpresent": { + "id": "396d478b-34aa-4afa-9898-cdce8222a516" + } + }, + "outputs": [], + "source": [ + "plt.figure(figsize = (20, 20))\n", + "for test_image in np.arange(3):\n", + " test_inputs[test_image].reshape(1, 64, 64)\n", + " plt.subplot(1, 8, test_image+1)\n", + " plt.axhline('')\n", + " plt.axvline('')\n", + " plt.text(x = 10, y = -10, s = test_outputs[test_image][0], fontsize = 18)\n", + " plt.imshow(test_inputs[test_image].reshape(64, 64))\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Run evaluation / prediction" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "plt.figure(figsize = (16, 6), frameon=False)\n", + "plt.subplot(1, 8, 1)\n", + "\n", + "plt.text(x = 0, y = -30, s = \"True Label: \", fontsize = 13, color = 'black')\n", + "plt.text(x = 0, y = -20, s = \"Result: \", fontsize = 13, color = 'black')\n", + "plt.text(x = 0, y = -10, s = \"Inference Time: \", fontsize = 13, color = 'black')\n", + "plt.text(x = 3, y = 14, s = \"Model Input\", fontsize = 12, color = 'black')\n", + "plt.text(x = 6, y = 18, s = \"(64 x 64)\", fontsize = 12, color = 'black')\n", + "plt.imshow(np.ones((28,28)), cmap=plt.cm.Greys) \n", + "\n", + "\n", + "for i in np.arange(test_data_size):\n", + " \n", + " input_data = json.dumps({'data': test_inputs[i].tolist()})\n", + "\n", + " # predict using the deployed model\n", + " r = json.loads(aci_service.run(input_data))\n", + " \n", + " if len(r) == 1:\n", + " print(r['error'])\n", + " break\n", + " \n", + " result = r['result']\n", + " time_ms = np.round(r['time'] * 1000, 2)\n", + " \n", + " ground_truth = int(np.argmax(test_outputs[i]))\n", + " \n", + " # compare actual value vs. the predicted values:\n", + " plt.subplot(1, 8, i+2)\n", + " plt.axhline('')\n", + " plt.axvline('')\n", + "\n", + " # use different color for misclassified sample\n", + " font_color = 'red' if ground_truth != result else 'black'\n", + " clr_map = plt.cm.gray if ground_truth != result else plt.cm.Greys\n", + "\n", + " # ground truth labels are in blue\n", + " plt.text(x = 10, y = -30, s = ground_truth, fontsize = 18, color = 'blue')\n", + " \n", + " # predictions are in black if correct, red if incorrect\n", + " plt.text(x = 10, y = -20, s = result, fontsize = 18, color = font_color)\n", + " plt.text(x = 5, y = -10, s = str(time_ms) + ' ms', fontsize = 14, color = font_color)\n", + "\n", + " \n", + " plt.imshow(test_inputs[i].reshape(64, 64), cmap = clr_map)\n", + "\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Try classifying your own images!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Replace the following string with your own path/test image\n", + "# Make sure the dimensions are 28 * 28 pixels\n", + "\n", + "# Any PNG or JPG image file should work\n", + "# Make sure to include the entire path with // instead of /\n", + "\n", + "# e.g. your_test_image = \"C://Users//vinitra.swamy//Pictures//emotion_test_images//img_1.jpg\"\n", + "\n", + "your_test_image = \"\"\n", + "\n", + "import matplotlib.image as mpimg\n", + "\n", + "if your_test_image != \"\":\n", + " img = mpimg.imread(your_test_image)\n", + " plt.subplot(1,3,1)\n", + " plt.imshow(img, cmap = plt.cm.Greys)\n", + " img = img.reshape(1, 1, 64, 64)\n", + "else:\n", + " img = None" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "if img is None:\n", + " print(\"Add the path for your image data.\")\n", + "else:\n", + " input_data = json.dumps({'data': img.tolist()})\n", + "\n", + " try:\n", + " r = json.loads(aci_service.run(input_data))\n", + " result = r['result']\n", + " time_ms = np.round(r['time'] * 1000, 2)\n", + " except Exception as e:\n", + " print(json.loads(r)['error'])\n", + "\n", + " plt.figure(figsize = (16, 6))\n", + " plt.subplot(1, 15,1)\n", + " plt.axhline('')\n", + " plt.axvline('')\n", + " plt.text(x = -100, y = -20, s = \"Model prediction: \", fontsize = 14)\n", + " plt.text(x = -100, y = -10, s = \"Inference time: \", fontsize = 14)\n", + " plt.text(x = 0, y = -20, s = str(result), fontsize = 14)\n", + " plt.text(x = 0, y = -10, s = str(time_ms) + \" ms\", fontsize = 14)\n", + " plt.text(x = -100, y = 14, s = \"Input image: \", fontsize = 14)\n", + " plt.imshow(img.reshape(28, 28), cmap = plt.cm.Greys) " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# remember to delete your service after you are done using it!\n", + "\n", + "# aci_service.delete()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Conclusion\n", + "\n", + "Congratulations!\n", + "\n", + "In this tutorial, you have managed to:\n", + "- familiarize yourself with the ONNX standard, ONNX Runtime inference, and the pretrained models in the ONNX model zoo\n", + "- understand a state-of-the-art convolutional neural net image classification model (FER+ in ONNX) and deploy it in the Azure ML cloud\n", + "- ensure that your deep learning model is working correctly (in the cloud) on test data, and check it against some of your own!\n", + "\n", + "Next steps:\n", + "- If you have not already, check out another interesting ONNX/AML application that lets you set up a state-of-the-art [handwritten image classification model (MNIST)](https://github.com/Azure/MachineLearningNotebooks/tree/master/onnx/onnx-inference-mnist.ipynb) in the cloud! This tutorial deploys a pre-trained ONNX Computer Vision model for handwritten digit classification in an Azure ML virtual machine.\n", + "- Contribute to our [open source ONNX repository on github](http://github.com/onnx/onnx) and/or add to our [ONNX model zoo](http://github.com/onnx/models)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python [conda env:finaldemo]", + "language": "python", + "name": "conda-env-finaldemo-py" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.6" + }, + "msauthor": "vinitra.swamy" + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/onnx/onnx-inference-mnist.ipynb b/onnx/onnx-inference-mnist.ipynb new file mode 100644 index 000000000..8514984e9 --- /dev/null +++ b/onnx/onnx-inference-mnist.ipynb @@ -0,0 +1,854 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Copyright (c) Microsoft Corporation. All rights reserved. \n", + "Licensed under the MIT License." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 01. Handwritten Digit Classification (MNIST) using ONNX Runtime on AzureML\n", + "\n", + "This example shows how to deploy an image classification neural network using the Modified National Institute of Standards and Technology ([MNIST](http://yann.lecun.com/exdb/mnist/)) dataset and Open Neural Network eXchange format ([ONNX](http://aka.ms/onnxdocarticle)) on the Azure Machine Learning platform. MNIST is a popular dataset consisting of 70,000 grayscale images. Each image is a handwritten digit of 28x28 pixels, representing number from 0 to 9. This tutorial will show you how to deploy a MNIST model from the [ONNX model zoo](https://github.com/onnx/models), use it to make predictions using ONNX Runtime Inference, and deploy it as a web service in Azure.\n", + "\n", + "Throughout this tutorial, we will be referring to ONNX, a neural network exchange format used to represent deep learning models. With ONNX, AI developers can more easily move models between state-of-the-art tools (CNTK, PyTorch, Caffe, MXNet, TensorFlow) and choose the combination that is best for them. ONNX is developed and supported by a community of partners including Microsoft AI, Facebook, and Amazon. For more information, explore the [ONNX website](http://onnx.ai) and [open source files](https://github.com/onnx).\n", + "\n", + "[ONNX Runtime](https://aka.ms/onnxruntime) is the runtime engine that enables evaluation of trained machine learning (Traditional ML and Deep Learning) models with high performance and low resource utilization.\n", + "\n", + "#### Tutorial Objectives:\n", + "\n", + "1. Describe the MNIST dataset and pretrained Convolutional Neural Net ONNX model, stored in the ONNX model zoo.\n", + "2. Deploy and run the pretrained MNIST ONNX model on an Azure Machine Learning instance\n", + "3. Predict labels for test set data points in the cloud using ONNX Runtime and Azure ML" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Prerequisites\n", + "\n", + "### 1. Install Azure ML SDK and create a new workspace\n", + "Please follow [00.configuration.ipynb](https://github.com/Azure/MachineLearningNotebooks/blob/master/00.configuration.ipynb) notebook.\n", + "\n", + "\n", + "### 2. Install additional packages needed for this Notebook\n", + "You need to install the popular plotting library `matplotlib` and the `onnx` library in the conda environment where Azure Maching Learning SDK is installed.\n", + "\n", + "```sh\n", + "(myenv) $ pip install matplotlib onnx\n", + "```\n", + "\n", + "### 3. Download sample data and pre-trained ONNX model from ONNX Model Zoo.\n", + "\n", + "[Download the ONNX MNIST model and corresponding test data](https://www.cntk.ai/OnnxModels/mnist/opset_7/mnist.tar.gz) and place them in the same folder as this tutorial notebook. You can unzip the file through the following line of code.\n", + "\n", + "```sh\n", + "(myenv) $ tar xvzf mnist.tar.gz\n", + "```\n", + "\n", + "More information can be found about the ONNX MNIST model on [github](https://github.com/onnx/models/tree/master/mnist). For more information about the MNIST dataset, please visit [Yan LeCun's website](http://yann.lecun.com/exdb/mnist/)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Load Azure ML workspace\n", + "\n", + "We begin by instantiating a workspace object from the existing workspace created earlier in the configuration notebook." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Check core SDK version number\n", + "import azureml.core\n", + "\n", + "print(\"SDK version:\", azureml.core.VERSION)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from azureml.core import Workspace\n", + "\n", + "ws = Workspace.from_config()\n", + "print(ws.name, ws.resource_group, ws.location, sep = '\\n')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Registering your model with Azure ML" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "model_dir = \"mnist\" # replace this with the location of your model files\n", + "\n", + "# leave as is if it's in the same folder as this notebook" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from azureml.core.model import Model\n", + "\n", + "model = Model.register(model_path = model_dir + \"//model.onnx\",\n", + " model_name = \"mnist_1\",\n", + " tags = {\"onnx\": \"demo\"},\n", + " description = \"MNIST image classification CNN from ONNX Model Zoo\",\n", + " workspace = ws)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Optional: Displaying your registered models\n", + "\n", + "This step is not required, so feel free to skip it." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "models = ws.models()\n", + "for m in models:\n", + " print(\"Name:\", m.name,\"\\tVersion:\", m.version, \"\\tDescription:\", m.description, m.tags)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "nbpresent": { + "id": "c3f2f57c-7454-4d3e-b38d-b0946cf066ea" + } + }, + "source": [ + "### ONNX MNIST Model Methodology\n", + "\n", + "The image classification model we are using is pre-trained using Microsoft's deep learning cognitive toolkit, [CNTK](https://github.com/Microsoft/CNTK), from the [ONNX model zoo](http://github.com/onnx/models). The model zoo has many other models that can be deployed on cloud providers like AzureML without any additional training. To ensure that our cloud deployed model works, we use testing data from the famous MNIST data set, provided as part of the [trained MNIST model](https://github.com/onnx/models/tree/master/mnist) in the ONNX model zoo.\n", + "\n", + "***Input: Handwritten Images from MNIST Dataset***\n", + "\n", + "***Task: Classify each MNIST image into an appropriate digit***\n", + "\n", + "***Output: Digit prediction for input image***\n", + "\n", + "Run the cell below to look at some of the sample images from the MNIST dataset that we used to train this ONNX model. Remember, once the application is deployed in Azure ML, you can use your own images as input for the model to classify!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# for images and plots in this notebook\n", + "import matplotlib.pyplot as plt \n", + "from IPython.display import Image\n", + "\n", + "# display images inline\n", + "%matplotlib inline" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Image(url=\"http://3.bp.blogspot.com/_UpN7DfJA0j4/TJtUBWPk0SI/AAAAAAAAABY/oWPMtmqJn3k/s1600/mnist_originals.png\", width=200, height=200)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Deploy our model on Azure ML" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We are now going to deploy our ONNX Model on AML with inference in ONNX Runtime. We begin by writing a score.py file, which will help us run the model in our Azure ML virtual machine (VM), and then specify our environment by writing a yml file.\n", + "\n", + "You will also notice that we import the onnxruntime library to do runtime inference on our ONNX models (passing in input and evaluating out model's predicted output). More information on the API and commands can be found in the [ONNX Runtime documentation](https://aka.ms/onnxruntime).\n", + "\n", + "### Write Score File\n", + "\n", + "A score file is what tells our Azure cloud service what to do. After initializing our model using azureml.core.model, we start an ONNX Runtime inference session to evaluate the data passed in on our function calls." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile score.py\n", + "import json\n", + "import numpy as np\n", + "import onnxruntime\n", + "import sys\n", + "import os\n", + "from azureml.core.model import Model\n", + "import time\n", + "\n", + "\n", + "def init():\n", + " global session\n", + " model = Model.get_model_path(model_name = 'mnist_1')\n", + " session = onnxruntime.InferenceSession(model, None)\n", + " \n", + "def run(input_data):\n", + " '''Purpose: evaluate test input in Azure Cloud using onnxruntime.\n", + " We will call the run function later from our Jupyter Notebook \n", + " so our azure service can evaluate our model input in the cloud. '''\n", + "\n", + " try:\n", + " # load in our data, convert to readable format\n", + " start = time.time()\n", + " data = np.array(json.loads(input_data)['data']).astype('float32')\n", + "\n", + " r = session.run([\"Plus214_Output_0\"], {\"Input3\": data})[0]\n", + " result = choose_class(r[0])\n", + " end = time.time()\n", + " result_dict = {\"result\": np.array(result).tolist(),\n", + " \"time\": np.array(end - start).tolist()}\n", + " except Exception as e:\n", + " result_dict = {\"error\": str(e)}\n", + " \n", + " return json.dumps(result_dict)\n", + "\n", + "def choose_class(result_prob):\n", + " \"\"\"We use argmax to determine the right label to choose from our output, after calling softmax on the 10 numbers we receive\"\"\"\n", + " return int(np.argmax(result_prob, axis=0))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Write Environment File" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This step creates a YAML file that specifies which dependencies we would like to see in our Linux Virtual Machine." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from azureml.core.conda_dependencies import CondaDependencies \n", + "\n", + "myenv = CondaDependencies()\n", + "myenv.add_pip_package(\"numpy\")\n", + "myenv.add_pip_package(\"azureml-core\")\n", + "myenv.add_pip_package(\"onnxruntime\")\n", + "\n", + "\n", + "with open(\"myenv.yml\",\"w\") as f:\n", + " f.write(myenv.serialize_to_string())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Create the Container Image\n", + "\n", + "This step will likely take a few minutes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from azureml.core.image import ContainerImage\n", + "\n", + "image_config = ContainerImage.image_configuration(execution_script = \"score.py\",\n", + " runtime = \"python\",\n", + " conda_file = \"myenv.yml\",\n", + " description = \"test\",\n", + " tags = {\"demo\": \"onnx\"} \n", + " )\n", + "\n", + "\n", + "image = ContainerImage.create(name = \"onnxtest\",\n", + " # this is the model object\n", + " models = [model],\n", + " image_config = image_config,\n", + " workspace = ws)\n", + "\n", + "image.wait_for_creation(show_output = True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Debugging\n", + "\n", + "In case you need to debug your code, the next line of code accesses the log file." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(image.image_build_log_uri)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We're all set! Let's get our model chugging.\n", + "\n", + "## Deploy the container image" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from azureml.core.webservice import AciWebservice\n", + "\n", + "aciconfig = AciWebservice.deploy_configuration(cpu_cores = 1, \n", + " memory_gb = 1, \n", + " tags = {'demo': 'onnx'}, \n", + " description = 'ONNX for mnist model')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The following cell will likely take a few minutes to run as well." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from azureml.core.webservice import Webservice\n", + "\n", + "aci_service_name = 'onnx-demo-mnist'\n", + "print(\"Service\", aci_service_name)\n", + "\n", + "aci_service = Webservice.deploy_from_image(deployment_config = aciconfig,\n", + " image = image,\n", + " name = aci_service_name,\n", + " workspace = ws)\n", + "\n", + "aci_service.wait_for_deployment(True)\n", + "print(aci_service.state)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "if aci_service.state != 'Healthy':\n", + " # run this command for debugging.\n", + " print(aci_service.get_logs())\n", + "\n", + " # If your deployment fails, make sure to delete your aci_service or rename your service before trying again!\n", + " # aci_service.delete()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Success!\n", + "\n", + "If you've made it this far, you've deployed a working VM with a handwritten digit classifier running in the cloud using Azure ML. Congratulations!\n", + "\n", + "Let's see how well our model deals with our test images." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Testing and Evaluation" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Load Test Data\n", + "\n", + "These are already in your directory from your ONNX model download (from the model zoo). If you didn't place your model and test data in the same directory as this notebook, edit the \"model_dir\" filename below." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# to manipulate our arrays\n", + "import numpy as np \n", + "\n", + "# read in test data protobuf files included with the model\n", + "import onnx\n", + "from onnx import numpy_helper\n", + "\n", + "# to use parsers to read in our model/data\n", + "import json\n", + "import os\n", + "\n", + "test_inputs = []\n", + "test_outputs = []\n", + "\n", + "# read in 3 testing images from .pb files\n", + "test_data_size = 3\n", + "\n", + "for i in np.arange(test_data_size):\n", + " input_test_data = os.path.join(model_dir, 'test_data_set_{0}'.format(i), 'input_0.pb')\n", + " output_test_data = os.path.join(model_dir, 'test_data_set_{0}'.format(i), 'output_0.pb')\n", + " \n", + " # convert protobuf tensors to np arrays using the TensorProto reader from ONNX\n", + " tensor = onnx.TensorProto()\n", + " with open(input_test_data, 'rb') as f:\n", + " tensor.ParseFromString(f.read())\n", + " \n", + " input_data = numpy_helper.to_array(tensor)\n", + " test_inputs.append(input_data)\n", + " \n", + " with open(output_test_data, 'rb') as f:\n", + " tensor.ParseFromString(f.read())\n", + " \n", + " output_data = numpy_helper.to_array(tensor)\n", + " test_outputs.append(output_data)\n", + " \n", + "if len(test_inputs) == test_data_size:\n", + " print('Test data loaded successfully.')" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "nbpresent": { + "id": "c3f2f57c-7454-4d3e-b38d-b0946cf066ea" + } + }, + "source": [ + "### Show some sample images\n", + "We use `matplotlib` to plot 3 test images from the dataset." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "nbpresent": { + "id": "396d478b-34aa-4afa-9898-cdce8222a516" + } + }, + "outputs": [], + "source": [ + "plt.figure(figsize = (16, 6))\n", + "for test_image in np.arange(3):\n", + " plt.subplot(1, 15, test_image+1)\n", + " plt.axhline('')\n", + " plt.axvline('')\n", + " plt.imshow(test_inputs[test_image].reshape(28, 28), cmap = plt.cm.Greys)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Run evaluation / prediction" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "plt.figure(figsize = (16, 6), frameon=False)\n", + "plt.subplot(1, 8, 1)\n", + "\n", + "plt.text(x = 0, y = -30, s = \"True Label: \", fontsize = 13, color = 'black')\n", + "plt.text(x = 0, y = -20, s = \"Result: \", fontsize = 13, color = 'black')\n", + "plt.text(x = 0, y = -10, s = \"Inference Time: \", fontsize = 13, color = 'black')\n", + "plt.text(x = 3, y = 14, s = \"Model Input\", fontsize = 12, color = 'black')\n", + "plt.text(x = 6, y = 18, s = \"(28 x 28)\", fontsize = 12, color = 'black')\n", + "plt.imshow(np.ones((28,28)), cmap=plt.cm.Greys) \n", + "\n", + "\n", + "for i in np.arange(test_data_size):\n", + " \n", + " input_data = json.dumps({'data': test_inputs[i].tolist()})\n", + " \n", + " # predict using the deployed model\n", + " r = json.loads(aci_service.run(input_data))\n", + " \n", + " if len(r) == 1:\n", + " print(r['error'])\n", + " break\n", + " \n", + " result = r['result']\n", + " time_ms = np.round(r['time'] * 1000, 2)\n", + " \n", + " ground_truth = int(np.argmax(test_outputs[i]))\n", + " \n", + " # compare actual value vs. the predicted values:\n", + " plt.subplot(1, 8, i+2)\n", + " plt.axhline('')\n", + " plt.axvline('')\n", + "\n", + " # use different color for misclassified sample\n", + " font_color = 'red' if ground_truth != result else 'black'\n", + " clr_map = plt.cm.gray if ground_truth != result else plt.cm.Greys\n", + "\n", + " # ground truth labels are in blue\n", + " plt.text(x = 10, y = -30, s = ground_truth, fontsize = 18, color = 'blue')\n", + " \n", + " # predictions are in black if correct, red if incorrect\n", + " plt.text(x = 10, y = -20, s = result, fontsize = 18, color = font_color)\n", + " plt.text(x = 5, y = -10, s = str(time_ms) + ' ms', fontsize = 14, color = font_color)\n", + "\n", + " \n", + " plt.imshow(test_inputs[i].reshape(28, 28), cmap = clr_map)\n", + "\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Try classifying your own images!\n", + "\n", + "Create your own 28 pixel by 28 pixel handwritten image and pass it into the model." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Preprocessing functions\n", + "\n", + "def rgb2gray(rgb):\n", + " \"\"\"Convert the input image into grayscale\"\"\"\n", + " return np.dot(rgb[...,:3], [0.299, 0.587, 0.114])\n", + "\n", + "def preprocess(img):\n", + " \"\"\"Resize input images and convert them to grayscale.\"\"\"\n", + " if img.shape[0] != 28:\n", + " print(\"Input image size is not 28 * 28 pixels. Please resize and try again.\")\n", + " grayscale = rgb2gray(img)\n", + " grayscale.resize((1, 1, 28, 28))\n", + " return grayscale" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Replace this string with your own path/test image\n", + "# Make sure the dimensions are 28 * 28 pixels\n", + "\n", + "# Any PNG or JPG image file should work\n", + "# Make sure to include the entire path with // instead of /\n", + "\n", + "# e.g. your_test_image = \"C://Users//vinitra.swamy//Pictures//digit.png\"\n", + "\n", + "your_test_image = \"\"\n", + "\n", + "import matplotlib.image as mpimg\n", + "\n", + "if your_test_image != \"\":\n", + " img = mpimg.imread(your_test_image)\n", + " plt.subplot(1,3,1)\n", + " plt.imshow(img, cmap = plt.cm.Greys)\n", + " print(\"Old Dimensions: \", img.shape)\n", + " img = preprocess(img)\n", + " print(\"New Dimensions: \", img.shape)\n", + "else:\n", + " img = None" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "if img is None:\n", + " print(\"Add the path for your image data.\")\n", + "else:\n", + " input_data = json.dumps({'data': img.tolist()})\n", + "\n", + " try:\n", + " r = json.loads(aci_service.run(input_data))\n", + " result = r['result']\n", + " time_ms = np.round(r['time'] * 1000, 2)\n", + " except Exception as e:\n", + " print(str(e), r['error'])\n", + "\n", + " plt.figure(figsize = (16, 6))\n", + " plt.subplot(1, 15,1)\n", + " plt.axhline('')\n", + " plt.axvline('')\n", + " plt.text(x = -100, y = -20, s = \"Model prediction: \", fontsize = 14)\n", + " plt.text(x = -100, y = -10, s = \"Inference time: \", fontsize = 14)\n", + " plt.text(x = 0, y = -20, s = str(result), fontsize = 14)\n", + " plt.text(x = 0, y = -10, s = str(time_ms) + \" ms\", fontsize = 14)\n", + " plt.text(x = -100, y = 14, s = \"Input image: \", fontsize = 14)\n", + " plt.imshow(img.reshape(28, 28), cmap = plt.cm.gray) " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Optional: How does our MNIST model work? \n", + "#### A brief explanation of Convolutional Neural Networks\n", + "\n", + "A [convolutional neural network](https://en.wikipedia.org/wiki/Convolutional_neural_network) (CNN, or ConvNet) is a type of [feed-forward](https://en.wikipedia.org/wiki/Feedforward_neural_network) artificial neural network made up of neurons that have learnable weights and biases. The CNNs take advantage of the spatial nature of the data. In nature, we perceive different objects by their shapes, size and colors. For example, objects in a natural scene are typically edges, corners/vertices (defined by two of more edges), color patches etc. These primitives are often identified using different detectors (e.g., edge detection, color detector) or combination of detectors interacting to facilitate image interpretation (object classification, region of interest detection, scene description etc.) in real world vision related tasks. These detectors are also known as filters. Convolution is a mathematical operator that takes an image and a filter as input and produces a filtered output (representing say edges, corners, or colors in the input image). \n", + "\n", + "Historically, these filters are a set of weights that were often hand crafted or modeled with mathematical functions (e.g., [Gaussian](https://en.wikipedia.org/wiki/Gaussian_filter) / [Laplacian](http://homepages.inf.ed.ac.uk/rbf/HIPR2/log.htm) / [Canny](https://en.wikipedia.org/wiki/Canny_edge_detector) filter). The filter outputs are mapped through non-linear activation functions mimicking human brain cells called [neurons](https://en.wikipedia.org/wiki/Neuron). Popular deep CNNs or ConvNets (such as [AlexNet](https://en.wikipedia.org/wiki/AlexNet), [VGG](https://arxiv.org/abs/1409.1556), [Inception](http://www.cv-foundation.org/openaccess/content_cvpr_2015/papers/Szegedy_Going_Deeper_With_2015_CVPR_paper.pdf), [ResNet](https://arxiv.org/pdf/1512.03385v1.pdf)) that are used for various [computer vision](https://en.wikipedia.org/wiki/Computer_vision) tasks have many of these architectural primitives (inspired from biology). \n", + "\n", + "### Convolution Layer\n", + "\n", + "A convolution layer is a set of filters. Each filter is defined by a weight (**W**) matrix, and bias ($b$).\n", + "\n", + "![](https://www.cntk.ai/jup/cntk103d_filterset_v2.png)\n", + "\n", + "These filters are scanned across the image performing the dot product between the weights and corresponding input value ($x$). The bias value is added to the output of the dot product and the resulting sum is optionally mapped through an activation function. This process is illustrated in the following animation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Image(url=\"https://www.cntk.ai/jup/cntk103d_conv2d_final.gif\", width= 200)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Model Description\n", + "\n", + "The MNIST model from the ONNX Model Zoo uses maxpooling to update the weights in its convolutions, summarized by the graphic below. You can see the entire workflow of our pre-trained model in the following image, with our input images and our output probabilities of each of our 10 labels. If you're interested in exploring the logic behind creating a Deep Learning model further, please look at the [training tutorial for our ONNX MNIST Convolutional Neural Network](https://github.com/Microsoft/CNTK/blob/master/Tutorials/CNTK_103D_MNIST_ConvolutionalNeuralNetwork.ipynb). " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Max-Pooling for Convolutional Neural Nets\n", + "\n", + "![](http://www.cntk.ai/jup/c103d_max_pooling.gif)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Pre-Trained Model Architecture\n", + "\n", + "![](http://www.cntk.ai/jup/conv103d_mnist-conv-mp.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Try classifying your own images!\n", + "\n", + "Create your own 28 pixel by 28 pixel handwritten image and pass it into the model." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Preprocessing functions\n", + "\n", + "def rgb2gray(rgb):\n", + " \"\"\"Convert the input image into grayscale\"\"\"\n", + " return np.dot(rgb[...,:3], [0.299, 0.587, 0.114])\n", + "\n", + "def preprocess(img):\n", + " \"\"\"Resize input images and convert them to grayscale.\"\"\"\n", + " if img.shape[0] != 28:\n", + " print(\"Input image size is not 28 * 28 pixels. Please resize and try again.\")\n", + " grayscale = rgb2gray(img)\n", + " grayscale.resize((1, 1, 28, 28))\n", + " return grayscale" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Replace this string with your own path/test image\n", + "# Make sure the dimensions are 28 * 28 pixels\n", + "\n", + "# Any PNG or JPG image file should work\n", + "# Make sure to include the entire path with // instead of /\n", + "\n", + "# e.g. your_test_image = \"C://Users//vinitra.swamy//Pictures//digit.png\"\n", + "\n", + "your_test_image = \"\"\n", + "\n", + "import matplotlib.image as mpimg\n", + "\n", + "if your_test_image != \"\":\n", + " img = mpimg.imread(your_test_image)\n", + " plt.subplot(1,3,1)\n", + " plt.imshow(img, cmap = plt.cm.Greys)\n", + " print(\"Old Dimensions: \", img.shape)\n", + " img = preprocess(img)\n", + " print(\"New Dimensions: \", img.shape)\n", + "else:\n", + " img = None" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "if img is None:\n", + " print(\"Add the path for your image data.\")\n", + "else:\n", + " input_data = json.dumps({'data': img.tolist()})\n", + "\n", + " try:\n", + " r = json.loads(aci_service.run(input_data))\n", + " result = r['result']\n", + " time_ms = np.round(r['time'] * 1000, 2)\n", + " except Exception as e:\n", + " print(str(e), r['error'])\n", + "\n", + " plt.figure(figsize = (16, 6))\n", + " plt.subplot(1, 15,1)\n", + " plt.axhline('')\n", + " plt.axvline('')\n", + " plt.text(x = -100, y = -20, s = \"Model prediction: \", fontsize = 14)\n", + " plt.text(x = -100, y = -10, s = \"Inference time: \", fontsize = 14)\n", + " plt.text(x = 0, y = -20, s = str(result), fontsize = 14)\n", + " plt.text(x = 0, y = -10, s = str(time_ms) + \" ms\", fontsize = 14)\n", + " plt.text(x = -100, y = 14, s = \"Input image: \", fontsize = 14)\n", + " plt.imshow(img.reshape(28, 28), cmap = plt.cm.gray) " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# remember to delete your service after you are done using it!\n", + "# uncomment the following line of code to delete your service\n", + "\n", + "# aci_service.delete()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Conclusion\n", + "\n", + "Congratulations!\n", + "\n", + "In this tutorial, you have managed to:\n", + "- familiarize yourself with the ONNX model format, ONNX Runtime inference, and the pretrained models in the ONNX model zoo\n", + "- understand a state-of-the-art convolutional neural net image classification model (MNIST in ONNX) and deploy it in the Azure ML cloud\n", + "- ensure that your deep learning model is working perfectly (in the cloud) on test data, and check it against some of your own!\n", + "\n", + "Next steps:\n", + "- Check out another interesting application based on a Microsoft Research computer vision paper that lets you set up a [facial emotion recognition model](https://github.com/Azure/MachineLearningNotebooks/tree/master/onnx/onnx-inference-emotion-recognition.ipynb) in the cloud! This tutorial deploys a pre-trained ONNX Computer Vision model in an Azure ML virtual machine with GPU support.\n", + "- Contribute to our [open source ONNX repository on github](http://github.com/onnx/onnx) and/or add to our [ONNX model zoo](http://github.com/onnx/models)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python [conda env:finaldemo]", + "language": "python", + "name": "conda-env-finaldemo-py" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.6" + }, + "msauthor": "vinitra.swamy" + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/pipeline/06.pipeline-batch-scoring.ipynb b/pipeline/pipeline-batch-scoring.ipynb similarity index 99% rename from pipeline/06.pipeline-batch-scoring.ipynb rename to pipeline/pipeline-batch-scoring.ipynb index bf53e4d95..d2b334350 100644 --- a/pipeline/06.pipeline-batch-scoring.ipynb +++ b/pipeline/pipeline-batch-scoring.ipynb @@ -375,7 +375,7 @@ "metadata": {}, "outputs": [], "source": [ - "node_run = list(pipeline_run.get_children())[0]" + "step_run = list(pipeline_run.get_children())[0]" ] }, { @@ -384,7 +384,7 @@ "metadata": {}, "outputs": [], "source": [ - "node_run.download_file(\"./outputs/result-labels.txt\")" + "step_run.download_file(\"./outputs/result-labels.txt\")" ] }, { @@ -522,7 +522,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.5" + "version": "3.6.3" } }, "nbformat": 4, diff --git a/training/01.train-tune-deploy-pytorch/01.train-tune-deploy-pytorch.ipynb b/training/01.train-tune-deploy-pytorch/01.train-tune-deploy-pytorch.ipynb new file mode 100644 index 000000000..1e6a6e16c --- /dev/null +++ b/training/01.train-tune-deploy-pytorch/01.train-tune-deploy-pytorch.ipynb @@ -0,0 +1,641 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Copyright (c) Microsoft Corporation. All rights reserved. \n", + "\n", + "Licensed under the MIT License." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 01. Train and deploy with PyTorch\n", + "\n", + "In this tutorial, you will train, hyperparameter tune, and deploy a PyTorch model using the Azure Machine Learning (AML) Python SDK.\n", + "\n", + "This tutorial will train an image classification model using transfer learning, based on PyTorch's [Transfer Learning tutorial](https://pytorch.org/tutorials/beginner/transfer_learning_tutorial.html). The model is trained to classify ants and bees by first using a pretrained ResNet18 model that has been trained on the [ImageNet](http://image-net.org/index) dataset." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Prerequisites\n", + "* Understand the [architecture and terms](https://docs.microsoft.com/azure/machine-learning/service/concept-azure-machine-learning-architecture) introduced by Azure Machine Learning\n", + "* Go through the [00.configuration.ipynb](https://github.com/Azure/MachineLearningNotebooks/blob/master/00.configuration.ipynb) notebook to:\n", + " * install the AML SDK\n", + " * create a workspace and its configuration file (`config.json`)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Check core SDK version number\n", + "import azureml.core\n", + "\n", + "print(\"SDK version:\", azureml.core.VERSION)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Initialize workspace\n", + "Initialize a [Workspace](https://docs.microsoft.com/azure/machine-learning/service/concept-azure-machine-learning-architecture#workspace) object from the existing workspace you created in the Prerequisites step. `Workspace.from_config()` creates a workspace object from the details stored in `config.json`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from azureml.core.workspace import Workspace\n", + "\n", + "ws = Workspace.from_config()\n", + "print('Workspace name: ' + ws.name, \n", + " 'Azure region: ' + ws.location, \n", + " 'Subscription id: ' + ws.subscription_id, \n", + " 'Resource group: ' + ws.resource_group, sep = '\\n')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Create a remote compute target\n", + "You will need to create a [compute target](https://docs.microsoft.com/azure/machine-learning/service/concept-azure-machine-learning-architecture#compute-target) to execute your training script on. In this tutorial, you create an [Azure Batch AI](https://docs.microsoft.com/azure/batch-ai/overview) cluster as your training compute resource. This code creates a cluster for you if it does not already exist in your workspace.\n", + "\n", + "**Creation of the cluster takes approximately 5 minutes.** If the cluster is already in your workspace this code will skip the cluster creation process." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from azureml.core.compute import ComputeTarget, BatchAiCompute\n", + "from azureml.core.compute_target import ComputeTargetException\n", + "\n", + "# choose a name for your cluster\n", + "cluster_name = \"gpucluster\"\n", + "\n", + "try:\n", + " compute_target = ComputeTarget(workspace=ws, name=cluster_name)\n", + " print('Found existing compute target.')\n", + "except ComputeTargetException:\n", + " print('Creating a new compute target...')\n", + " compute_config = BatchAiCompute.provisioning_configuration(vm_size='STANDARD_NC6', \n", + " autoscale_enabled=True,\n", + " cluster_min_nodes=0, \n", + " cluster_max_nodes=4)\n", + "\n", + " # create the cluster\n", + " compute_target = ComputeTarget.create(ws, cluster_name, compute_config)\n", + "\n", + " compute_target.wait_for_completion(show_output=True)\n", + "\n", + " # Use the 'status' property to get a detailed status for the current cluster. \n", + " print(compute_target.status.serialize())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The above code creates a GPU cluster. If you instead want to create a CPU cluster, provide a different VM size to the `vm_size` parameter, such as `STANDARD_D2_V2`." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Upload training data\n", + "The dataset we will use consists of about 120 training images each for ants and bees, with 75 validation images for each class." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "First, download the dataset (located [here](https://download.pytorch.org/tutorial/hymenoptera_data.zip) as a zip file) locally to your current directory and extract the files. This will create a folder called `hymenoptera_data` with two subfolders `train` and `val` that contain the training and validation images, respectively. [Hymenoptera](https://en.wikipedia.org/wiki/Hymenoptera) is the order of insects that includes ants and bees." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import urllib\n", + "from zipfile import ZipFile\n", + "\n", + "# download data\n", + "download_url = 'https://download.pytorch.org/tutorial/hymenoptera_data.zip'\n", + "data_file = './hymenoptera_data.zip'\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()\n", + " print('done')\n", + " \n", + "# delete zip file\n", + "os.remove(data_file)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To make the data accessible for remote training, you will need to upload the data from your local machine to the cloud. AML provides a convenient way to do so via a [Datastore](https://docs.microsoft.com/azure/machine-learning/service/how-to-access-data). The datastore provides a mechanism for you to upload/download data, and interact with it from your remote compute targets. \n", + "\n", + "**Note: If your data is already stored in Azure, or you download the data as part of your training script, you will not need to do this step.**\n", + "\n", + "Each workspace is associated with a default datastore. In this tutorial, we will upload the training data to this default datastore." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "ds = ws.get_default_datastore()\n", + "print(ds.datastore_type, ds.account_name, ds.container_name)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The following code will upload the training data to the path `./hymenoptera_data` on the default datastore." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "ds.upload(src_dir='./hymenoptera_data', target_path='hymenoptera_data')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now let's get a reference to the path on the datastore with the training data. We can do so using the `path` method. In the next section, we can then pass this reference to our training script's `--data_dir` argument. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "path_on_datastore = 'hymenoptera_data'\n", + "ds_data = ds.path(path_on_datastore)\n", + "print(ds_data)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Train model on the remote compute\n", + "Now that you have your data and training script prepared, you are ready to train on your remote compute cluster. You can take advantage of Azure compute to leverage GPUs to cut down your training time. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Create a project directory\n", + "Create a directory that will contain all the necessary code from your local machine that you will need access to on the remote resource. This includes the training script and any additional files your training script depends on." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "\n", + "project_folder = './pytorch-hymenoptera'\n", + "os.makedirs(project_folder, exist_ok=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Prepare training script\n", + "Now you will need to create your training script. In this tutorial, the training script is already provided for you at `pytorch_train.py`. In practice, you should be able to take any custom training script as is and run it with AML without having to modify your code.\n", + "\n", + "However, if you would like to use AML's [tracking and metrics](https://docs.microsoft.com/azure/machine-learning/service/concept-azure-machine-learning-architecture#metrics) capabilities, you will have to add a small amount of AML code inside your training script. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Copy the training script `pytorch_train.py` into your project directory." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import shutil\n", + "shutil.copy('pytorch_train.py', project_folder)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Create an experiment\n", + "Create an [Experiment](https://docs.microsoft.com/azure/machine-learning/service/concept-azure-machine-learning-architecture#experiment) to track all the runs in your workspace for this transfer learning PyTorch tutorial. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from azureml.core import Experiment\n", + "\n", + "experiment_name = 'pytorch-hymenoptera'\n", + "experiment = Experiment(ws, name=experiment_name)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Create a PyTorch estimator\n", + "The AML SDK's PyTorch estimator enables you to easily submit PyTorch training jobs for both single-node and distributed runs. For more information on the PyTorch estimator, refer [here](https://docs.microsoft.com/azure/machine-learning/service/how-to-train-pytorch). The following code will define a single-node PyTorch job." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from azureml.train.dnn import PyTorch\n", + "\n", + "script_params = {\n", + " '--data_dir': ds_data,\n", + " '--num_epochs': 25,\n", + " '--output_dir': './outputs'\n", + "}\n", + "\n", + "estimator = PyTorch(source_directory=project_folder, \n", + " script_params=script_params,\n", + " compute_target=compute_target,\n", + " entry_script='pytorch_train.py',\n", + " use_gpu=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `script_params` parameter is a dictionary containing the command-line arguments to your training script `entry_script`. Please note the following:\n", + "- We passed our training data reference `ds_data` to our script's `--data_dir` argument. This will 1) mount our datastore on the remote compute and 2) provide the path to the training data `hymenoptera_data` on our datastore.\n", + "- We specified the output directory as `./outputs`. The `outputs` directory is specially treated by AML in that all the content in this directory gets uploaded to your workspace as part of your run history. The files written to this directory are therefore accessible even once your remote run is over. In this tutorial, we will save our trained model to this output directory.\n", + "\n", + "To leverage the Azure VM's GPU for training, we set `use_gpu=True`." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Submit job\n", + "Run your experiment by submitting your estimator object. Note that this call is asynchronous." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "run = experiment.submit(estimator)\n", + "print(run.get_details())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Monitor your run\n", + "You can monitor the progress of the run with a Jupyter widget. Like the run submission, the widget is asynchronous and provides live updates every 10-15 seconds until the job completes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from azureml.train.widgets import RunDetails\n", + "RunDetails(run).show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Register the trained model\n", + "Finally, register the trained model from your run to your workspace. The `model_path` parameter takes in the relative path on the remote VM to the model in your `outputs` directory. In the next section, we will deploy this registered model as a web service." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "model = run.register_model(model_name = 'pytorch-hymenoptera', model_path = 'outputs/model.pt')\n", + "print(model.name, model.id, model.version, sep = '\\t')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Deploy model as web service\n", + "Once you have your trained model, you can deploy the model on Azure. In this tutorial, we will deploy the model as a web service in [Azure Container Instances](https://docs.microsoft.com/en-us/azure/container-instances/) (ACI). For more information on deploying models using Azure ML, refer [here](https://docs.microsoft.com/azure/machine-learning/service/how-to-deploy-and-where)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Create scoring script\n", + "\n", + "First, we will create a scoring script that will be invoked by the web service call. Note that the scoring script must have two required functions:\n", + "* `init()`: In this function, you typically load the model into a `global` object. This function is executed only once when the Docker container is started. \n", + "* `run(input_data)`: In this function, the model is used to predict a value based on the input data. The input and output typically use JSON as serialization and deserialization format, but you are not limited to that.\n", + "\n", + "Refer to the scoring script `pytorch_score.py` for this tutorial. Our web service will use this file to predict whether an image is an ant or a bee. When writing your own scoring script, don't forget to test it locally first before you go and deploy the web service." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Create environment file\n", + "Then, we will need to create an environment file (`myenv.yml`) that specifies all of the scoring script's package dependencies. This file is used to ensure that all of those dependencies are installed in the Docker image by AML. In this case, we need to specify `torch`, `torchvision`, `pillow`, and `azureml-sdk`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile myenv.yml\n", + "name: myenv\n", + "channels:\n", + " - defaults\n", + "dependencies:\n", + " - pip:\n", + " - torch\n", + " - torchvision\n", + " - pillow\n", + " # Required packages for AzureML execution, history, and data preparation.\n", + " - --extra-index-url https://azuremlsdktestpypi.azureedge.net/sdk-release/Preview/E7501C02541B433786111FE8E140CAA1\n", + " - azureml-core" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Configure the container image\n", + "Now configure the Docker image that you will use to build your ACI container." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from azureml.core.image import ContainerImage\n", + "\n", + "image_config = ContainerImage.image_configuration(execution_script='pytorch_score.py', \n", + " runtime='python', \n", + " conda_file='myenv.yml',\n", + " description='Image with hymenoptera model')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Configure the ACI container\n", + "We are almost ready to deploy. Create a deployment configuration file to specify the number of CPUs and gigabytes of RAM needed for your ACI container. While it depends on your model, the default of `1` core and `1` gigabyte of RAM is usually sufficient for many models." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from azureml.core.webservice import AciWebservice\n", + "\n", + "aciconfig = AciWebservice.deploy_configuration(cpu_cores=1, \n", + " memory_gb=1, \n", + " tags={'data': 'hymenoptera', 'method':'transfer learning', 'framework':'pytorch'},\n", + " description='Classify ants/bees using transfer learning with PyTorch')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Deploy the registered model\n", + "Finally, let's deploy a web service from our registered model. First, retrieve the model from your workspace." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from azureml.core.model import Model\n", + "\n", + "model = Model(ws, name='pytorch-hymenoptera')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Then, deploy the web service using the ACI config and image config files created in the previous steps. We pass the `model` object in a list to the `models` parameter. If you would like to deploy more than one registered model, append the additional models to this list." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%time\n", + "from azureml.core.webservice import Webservice\n", + "\n", + "service_name = 'aci-hymenoptera'\n", + "service = Webservice.deploy_from_model(workspace=ws,\n", + " name=service_name,\n", + " models=[model],\n", + " image_config=image_config,\n", + " deployment_config=aciconfig,)\n", + "\n", + "service.wait_for_deployment(show_output=True)\n", + "print(service.state)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If your deployment fails for any reason and you need to redeploy, make sure to delete the service before you do so: `service.delete()`" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To get the logs from the deployment process, run the following command:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "service.get_logs()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Get the web service's HTTP endpoint, which accepts REST client calls. This endpoint can be shared with anyone who wants to test the web service or integrate it into an application." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(service.scoring_uri)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Test the web service\n", + "Finally, let's test our deployed web service. We will send the data as a JSON string to the web service hosted in ACI and use the SDK's `run` API to invoke the service. Here we will take an arbitrary image from our validation data to predict on." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os, json, base64\n", + "from io import BytesIO\n", + "from PIL import Image\n", + "import matplotlib.pyplot as plt\n", + "\n", + "def imgToBase64(img):\n", + " \"\"\"Convert pillow image to base64-encoded image\"\"\"\n", + " imgio = BytesIO()\n", + " img.save(imgio, 'JPEG')\n", + " img_str = base64.b64encode(imgio.getvalue())\n", + " return img_str.decode('utf-8')\n", + "\n", + "test_img = os.path.join('hymenoptera_data', 'val', 'bees', '10870992_eebeeb3a12.jpg') #arbitary image from val dataset\n", + "plt.imshow(Image.open(test_img))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "base64Img = imgToBase64(Image.open(test_img))\n", + "\n", + "result = service.run(input_data=json.dumps({'data': base64Img}))\n", + "print(json.loads(result))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Delete web service\n", + "Once you no longer need the web service, you should delete it." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "service.delete()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python [conda env:amlsdk]", + "language": "python", + "name": "conda-env-amlsdk-py" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.6" + }, + "msauthor": "minxia" + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/training/01.train-tune-deploy-pytorch/pytorch_score.py b/training/01.train-tune-deploy-pytorch/pytorch_score.py new file mode 100644 index 000000000..7bed01a8a --- /dev/null +++ b/training/01.train-tune-deploy-pytorch/pytorch_score.py @@ -0,0 +1,59 @@ +# Copyright (c) Microsoft. All rights reserved. +# Licensed under the MIT license. + +import torch +import torch.nn as nn +import torchvision +from torchvision import transforms +import os +import json +import base64 +from io import BytesIO +from PIL import Image + +from azureml.core.model import Model + + +def preprocess_image(image_file): + """Preprocess the input image.""" + data_transforms = transforms.Compose([ + transforms.Resize(256), + transforms.CenterCrop(224), + transforms.ToTensor(), + transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) + ]) + + image = Image.open(image_file) + image = data_transforms(image).float() + image = torch.tensor(image) + image = image.unsqueeze(0) + return image + + +def base64ToImg(base64ImgString): + base64Img = base64ImgString.encode('utf-8') + decoded_img = base64.b64decode(base64Img) + return BytesIO(decoded_img) + + +def init(): + global model + model_path = Model.get_model_path('pytorch-hymenoptera') + model = torch.load(model_path, map_location=lambda storage, loc: storage) + model.eval() + + +def run(input_data): + img = base64ToImg(json.loads(input_data)['data']) + img = preprocess_image(img) + + # get prediction + output = model(img) + + classes = ['ants', 'bees'] + softmax = nn.Softmax(dim=1) + pred_probs = softmax(model(img)).detach().numpy()[0] + index = torch.argmax(output, 1) + + result = json.dumps({"label": classes[index], "probability": str(pred_probs[index])}) + return result diff --git a/training/01.train-tune-deploy-pytorch/pytorch_train.py b/training/01.train-tune-deploy-pytorch/pytorch_train.py new file mode 100644 index 000000000..364732320 --- /dev/null +++ b/training/01.train-tune-deploy-pytorch/pytorch_train.py @@ -0,0 +1,169 @@ +# Copyright (c) 2017, PyTorch contributors +# Licensed under the BSD license + +# Adapted from https://pytorch.org/tutorials/beginner/transfer_learning_tutorial.html + +from __future__ import print_function, division + +import torch +import torch.nn as nn +import torch.optim as optim +from torch.optim import lr_scheduler +import torchvision +from torchvision import datasets, models, transforms +import numpy as np +import time +import os +import copy +import argparse + + +def load_data(data_dir): + """Load the train/val data.""" + + # Data augmentation and normalization for training + # Just normalization for validation + data_transforms = { + 'train': transforms.Compose([ + transforms.RandomResizedCrop(224), + transforms.RandomHorizontalFlip(), + transforms.ToTensor(), + transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) + ]), + 'val': transforms.Compose([ + transforms.Resize(256), + transforms.CenterCrop(224), + transforms.ToTensor(), + transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) + ]), + } + + image_datasets = {x: datasets.ImageFolder(os.path.join(data_dir, x), + data_transforms[x]) + for x in ['train', 'val']} + dataloaders = {x: torch.utils.data.DataLoader(image_datasets[x], batch_size=4, + shuffle=True, num_workers=0) + for x in ['train', 'val']} + dataset_sizes = {x: len(image_datasets[x]) for x in ['train', 'val']} + class_names = image_datasets['train'].classes + + return dataloaders, dataset_sizes, class_names + + +def train_model(model, criterion, optimizer, scheduler, num_epochs, data_dir): + """Train the model.""" + + # load training/validation data + dataloaders, dataset_sizes, class_names = load_data(data_dir) + + device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu') + since = time.time() + + best_model_wts = copy.deepcopy(model.state_dict()) + best_acc = 0.0 + + for epoch in range(num_epochs): + print('Epoch {}/{}'.format(epoch, num_epochs - 1)) + print('-' * 10) + + # Each epoch has a training and validation phase + for phase in ['train', 'val']: + if phase == 'train': + scheduler.step() + model.train() # Set model to training mode + else: + model.eval() # Set model to evaluate mode + + running_loss = 0.0 + running_corrects = 0 + + # Iterate over data. + for inputs, labels in dataloaders[phase]: + inputs = inputs.to(device) + labels = labels.to(device) + + # zero the parameter gradients + optimizer.zero_grad() + + # forward + # track history if only in train + with torch.set_grad_enabled(phase == 'train'): + outputs = model(inputs) + _, preds = torch.max(outputs, 1) + loss = criterion(outputs, labels) + + # backward + optimize only if in training phase + if phase == 'train': + loss.backward() + optimizer.step() + + # statistics + running_loss += loss.item() * inputs.size(0) + running_corrects += torch.sum(preds == labels.data) + + epoch_loss = running_loss / dataset_sizes[phase] + epoch_acc = running_corrects.double() / dataset_sizes[phase] + + print('{} Loss: {:.4f} Acc: {:.4f}'.format( + phase, epoch_loss, epoch_acc)) + + # deep copy the model + if phase == 'val' and epoch_acc > best_acc: + best_acc = epoch_acc + best_model_wts = copy.deepcopy(model.state_dict()) + + print() + + time_elapsed = time.time() - since + print('Training complete in {:.0f}m {:.0f}s'.format( + time_elapsed // 60, time_elapsed % 60)) + print('Best val Acc: {:4f}'.format(best_acc)) + + # load best model weights + model.load_state_dict(best_model_wts) + return model + + +def fine_tune_model(num_epochs, data_dir): + """Load a pretrained model and reset the final fully connected layer.""" + + model_ft = models.resnet18(pretrained=True) + num_ftrs = model_ft.fc.in_features + model_ft.fc = nn.Linear(num_ftrs, 2) # only 2 classes to predict + + device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu') + model_ft = model_ft.to(device) + + criterion = nn.CrossEntropyLoss() + + # Observe that all parameters are being optimized + optimizer_ft = optim.SGD(model_ft.parameters(), lr=0.001, momentum=0.9) + + # Decay LR by a factor of 0.1 every 7 epochs + exp_lr_scheduler = lr_scheduler.StepLR(optimizer_ft, step_size=7, gamma=0.1) + + model = train_model(model_ft, criterion, optimizer_ft, exp_lr_scheduler, num_epochs, data_dir) + + return model + + +def main(): + for root, dirs, files in os.walk("."): + print(root) + print(dirs) + + # get command-line arguments + parser = argparse.ArgumentParser() + parser.add_argument('--data_dir', type=str, help='directory of training data') + parser.add_argument('--num_epochs', type=int, default=25, help='number of epochs to train') + parser.add_argument('--output_dir', type=str, help='output directory') + args = parser.parse_args() + + print("data directory is: " + args.data_dir) + model = fine_tune_model(args.num_epochs, args.data_dir) + os.makedirs(args.output_dir, exist_ok=True) + torch.save(model, os.path.join(args.output_dir, 'model.pt')) + + +if __name__ == "__main__": + main() diff --git a/training/02.distributed-pytorch-with-horovod/02.distributed-pytorch-with-horovod.ipynb b/training/02.distributed-pytorch-with-horovod/02.distributed-pytorch-with-horovod.ipynb new file mode 100644 index 000000000..da7c539fc --- /dev/null +++ b/training/02.distributed-pytorch-with-horovod/02.distributed-pytorch-with-horovod.ipynb @@ -0,0 +1,289 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Copyright (c) Microsoft Corporation. All rights reserved.\n", + "\n", + "Licensed under the MIT License." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 02. Distributed PyTorch with Horovod\n", + "In this tutorial, you will train a PyTorch model on the [MNIST](http://yann.lecun.com/exdb/mnist/) dataset using distributed training via [Horovod](https://github.com/uber/horovod)." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Prerequisites\n", + "* Understand the [architecture and terms](https://docs.microsoft.com/azure/machine-learning/service/concept-azure-machine-learning-architecture) introduced by Azure Machine Learning (AML)\n", + "* Go through the [00.configuration.ipynb](https://github.com/Azure/MachineLearningNotebooks/blob/master/00.configuration.ipynb) notebook to:\n", + " * install the AML SDK\n", + " * create a workspace and its configuration file (`config.json`)\n", + "* Review the [tutorial](https://aka.ms/aml-notebook-pytorch) on single-node PyTorch training using the SDK" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Check core SDK version number\n", + "import azureml.core\n", + "\n", + "print(\"SDK version:\", azureml.core.VERSION)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Initialize workspace\n", + "\n", + "Initialize a [Workspace](https://docs.microsoft.com/azure/machine-learning/service/concept-azure-machine-learning-architecture#workspace) object from the existing workspace you created in the Prerequisites step. `Workspace.from_config()` creates a workspace object from the details stored in `config.json`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from azureml.core.workspace import Workspace\n", + "\n", + "ws = Workspace.from_config()\n", + "print('Workspace name: ' + ws.name, \n", + " 'Azure region: ' + ws.location, \n", + " 'Subscription id: ' + ws.subscription_id, \n", + " 'Resource group: ' + ws.resource_group, sep = '\\n')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Create a remote compute target\n", + "You will need to create a [compute target](https://docs.microsoft.com/azure/machine-learning/service/concept-azure-machine-learning-architecture#compute-target) to execute your training script on. In this tutorial, you create an [Azure Batch AI](https://docs.microsoft.com/azure/batch-ai/overview) cluster as your training compute resource. This code creates a cluster for you if it does not already exist in your workspace.\n", + "\n", + "**Creation of the cluster takes approximately 5 minutes.** If the cluster is already in your workspace this code will skip the cluster creation process." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from azureml.core.compute import ComputeTarget, BatchAiCompute\n", + "from azureml.core.compute_target import ComputeTargetException\n", + "\n", + "# choose a name for your cluster\n", + "cluster_name = \"gpucluster\"\n", + "\n", + "try:\n", + " compute_target = ComputeTarget(workspace=ws, name=cluster_name)\n", + " print('Found existing compute target.')\n", + "except ComputeTargetException:\n", + " print('Creating a new compute target...')\n", + " compute_config = BatchAiCompute.provisioning_configuration(vm_size='STANDARD_NC6', \n", + " autoscale_enabled=True,\n", + " cluster_min_nodes=0, \n", + " cluster_max_nodes=4)\n", + "\n", + " # create the cluster\n", + " compute_target = ComputeTarget.create(ws, cluster_name, compute_config)\n", + "\n", + " compute_target.wait_for_completion(show_output=True)\n", + "\n", + " # Use the 'status' property to get a detailed status for the current cluster. \n", + " print(compute_target.status.serialize())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The above code creates a GPU cluster. If you instead want to create a CPU cluster, provide a different VM size to the `vm_size` parameter, such as `STANDARD_D2_V2`." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Train model on the remote compute\n", + "Now that we have the cluster ready to go, let's run our distributed training job." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Create a project directory\n", + "Create a directory that will contain all the necessary code from your local machine that you will need access to on the remote resource. This includes the training script and any additional files your training script depends on." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "\n", + "project_folder = './pytorch-distr-hvd'\n", + "os.makedirs(project_folder, exist_ok=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Copy the training script `pytorch_horovod_mnist.py` into this project directory." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import shutil\n", + "shutil.copy('pytorch_horovod_mnist.py', project_folder)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Create an experiment\n", + "Create an [Experiment](https://docs.microsoft.com/azure/machine-learning/service/concept-azure-machine-learning-architecture#experiment) to track all the runs in your workspace for this distributed PyTorch tutorial. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from azureml.core import Experiment\n", + "\n", + "experiment_name = 'pytorch-distr-hvd'\n", + "experiment = Experiment(ws, name=experiment_name)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Create a PyTorch estimator\n", + "The AML SDK's PyTorch estimator enables you to easily submit PyTorch training jobs for both single-node and distributed runs. For more information on the PyTorch estimator, refer [here](https://docs.microsoft.com/azure/machine-learning/service/how-to-train-pytorch)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from azureml.train.dnn import PyTorch\n", + "\n", + "estimator = PyTorch(source_directory=project_folder,\n", + " compute_target=compute_target,\n", + " entry_script='pytorch_horovod_mnist.py',\n", + " node_count=2,\n", + " process_count_per_node=1,\n", + " distributed_backend='mpi',\n", + " use_gpu=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The above code specifies that we will run our training script on `2` nodes, with one worker per node. In order to execute a distributed run using MPI/Horovod, you must provide the argument `distributed_backend='mpi'`. Using this estimator with these settings, PyTorch, Horovod and their dependencies will be installed for you. However, if your script also uses other packages, make sure to install them via the `PyTorch` constructor's `pip_packages` or `conda_packages` parameters." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Submit job\n", + "Run your experiment by submitting your estimator object. Note that this call is asynchronous." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "run = experiment.submit(estimator)\n", + "print(run.get_details())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Monitor your run\n", + "You can monitor the progress of the run with a Jupyter widget. Like the run submission, the widget is asynchronous and provides live updates every 10-15 seconds until the job completes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from azureml.train.widgets import RunDetails\n", + "RunDetails(run).show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Alternatively, you can block until the script has completed training before running more code." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "run.wait_for_completion(show_output=True) # this provides a verbose log" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python [default]", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.6" + }, + "msauthor": "minxia" + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/training/02.distributed-pytorch-with-horovod/pytorch_horovod_mnist.py b/training/02.distributed-pytorch-with-horovod/pytorch_horovod_mnist.py new file mode 100644 index 000000000..a513cff97 --- /dev/null +++ b/training/02.distributed-pytorch-with-horovod/pytorch_horovod_mnist.py @@ -0,0 +1,157 @@ +# Copyright 2017 Uber Technologies, Inc. +# Licensed under the Apache License, Version 2.0 +# Script from horovod/examples: https://github.com/uber/horovod/blob/master/examples/pytorch_mnist.py + +from __future__ import print_function +import argparse +import torch.nn as nn +import torch.nn.functional as F +import torch.optim as optim +from torchvision import datasets, transforms +from torch.autograd import Variable +import torch.utils.data.distributed +import horovod.torch as hvd + +# Training settings +parser = argparse.ArgumentParser(description='PyTorch MNIST Example') +parser.add_argument('--batch-size', type=int, default=64, metavar='N', + help='input batch size for training (default: 64)') +parser.add_argument('--test-batch-size', type=int, default=1000, metavar='N', + help='input batch size for testing (default: 1000)') +parser.add_argument('--epochs', type=int, default=10, metavar='N', + help='number of epochs to train (default: 10)') +parser.add_argument('--lr', type=float, default=0.01, metavar='LR', + help='learning rate (default: 0.01)') +parser.add_argument('--momentum', type=float, default=0.5, metavar='M', + help='SGD momentum (default: 0.5)') +parser.add_argument('--no-cuda', action='store_true', default=False, + help='disables CUDA training') +parser.add_argument('--seed', type=int, default=42, metavar='S', + help='random seed (default: 42)') +parser.add_argument('--log-interval', type=int, default=10, metavar='N', + help='how many batches to wait before logging training status') +args = parser.parse_args() +args.cuda = not args.no_cuda and torch.cuda.is_available() + +hvd.init() +torch.manual_seed(args.seed) + +if args.cuda: + # Horovod: pin GPU to local rank. + torch.cuda.set_device(hvd.local_rank()) + torch.cuda.manual_seed(args.seed) + + +kwargs = {'num_workers': 1, 'pin_memory': True} if args.cuda else {} +train_dataset = \ + datasets.MNIST('data-%d' % hvd.rank(), train=True, download=True, + transform=transforms.Compose([ + transforms.ToTensor(), + transforms.Normalize((0.1307,), (0.3081,)) + ])) +train_sampler = torch.utils.data.distributed.DistributedSampler( + train_dataset, num_replicas=hvd.size(), rank=hvd.rank()) +train_loader = torch.utils.data.DataLoader( + train_dataset, batch_size=args.batch_size, sampler=train_sampler, **kwargs) + +test_dataset = \ + datasets.MNIST('data-%d' % hvd.rank(), train=False, transform=transforms.Compose([ + transforms.ToTensor(), + transforms.Normalize((0.1307,), (0.3081,)) + ])) +test_sampler = torch.utils.data.distributed.DistributedSampler( + test_dataset, num_replicas=hvd.size(), rank=hvd.rank()) +test_loader = torch.utils.data.DataLoader(test_dataset, batch_size=args.test_batch_size, + sampler=test_sampler, **kwargs) + + +class Net(nn.Module): + def __init__(self): + super(Net, self).__init__() + self.conv1 = nn.Conv2d(1, 10, kernel_size=5) + self.conv2 = nn.Conv2d(10, 20, kernel_size=5) + self.conv2_drop = nn.Dropout2d() + self.fc1 = nn.Linear(320, 50) + self.fc2 = nn.Linear(50, 10) + + def forward(self, x): + x = F.relu(F.max_pool2d(self.conv1(x), 2)) + x = F.relu(F.max_pool2d(self.conv2_drop(self.conv2(x)), 2)) + x = x.view(-1, 320) + x = F.relu(self.fc1(x)) + x = F.dropout(x, training=self.training) + x = self.fc2(x) + return F.log_softmax(x) + + +model = Net() + +if args.cuda: + # Move model to GPU. + model.cuda() + +# Horovod: broadcast parameters. +hvd.broadcast_parameters(model.state_dict(), root_rank=0) + +# Horovod: scale learning rate by the number of GPUs. +optimizer = optim.SGD(model.parameters(), lr=args.lr * hvd.size(), + momentum=args.momentum) + +# Horovod: wrap optimizer with DistributedOptimizer. +optimizer = hvd.DistributedOptimizer( + optimizer, named_parameters=model.named_parameters()) + + +def train(epoch): + model.train() + train_sampler.set_epoch(epoch) + for batch_idx, (data, target) in enumerate(train_loader): + if args.cuda: + data, target = data.cuda(), target.cuda() + data, target = Variable(data), Variable(target) + optimizer.zero_grad() + output = model(data) + loss = F.nll_loss(output, target) + loss.backward() + optimizer.step() + if batch_idx % args.log_interval == 0: + print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format( + epoch, batch_idx * len(data), len(train_sampler), + 100. * batch_idx / len(train_loader), loss.data[0])) + + +def metric_average(val, name): + tensor = torch.FloatTensor([val]) + avg_tensor = hvd.allreduce(tensor, name=name) + return avg_tensor[0] + + +def test(): + model.eval() + test_loss = 0. + test_accuracy = 0. + for data, target in test_loader: + if args.cuda: + data, target = data.cuda(), target.cuda() + data, target = Variable(data, volatile=True), Variable(target) + output = model(data) + # sum up batch loss + test_loss += F.nll_loss(output, target, size_average=False).data[0] + # get the index of the max log-probability + pred = output.data.max(1, keepdim=True)[1] + test_accuracy += pred.eq(target.data.view_as(pred)).cpu().float().sum() + + test_loss /= len(test_sampler) + test_accuracy /= len(test_sampler) + + test_loss = metric_average(test_loss, 'avg_loss') + test_accuracy = metric_average(test_accuracy, 'avg_accuracy') + + if hvd.rank() == 0: + print('\nTest set: Average loss: {:.4f}, Accuracy: {:.2f}%\n'.format( + test_loss, 100. * test_accuracy)) + + +for epoch in range(1, args.epochs + 1): + train(epoch) + test() diff --git a/training/04.distributed-tensorflow-with-horovod/04.distributed-tensorflow-with-horovod.ipynb b/training/04.distributed-tensorflow-with-horovod/04.distributed-tensorflow-with-horovod.ipynb new file mode 100644 index 000000000..221444e0a --- /dev/null +++ b/training/04.distributed-tensorflow-with-horovod/04.distributed-tensorflow-with-horovod.ipynb @@ -0,0 +1,360 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Copyright (c) Microsoft Corporation. All rights reserved.\n", + "\n", + "Licensed under the MIT License." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 04. Distributed Tensorflow with Horovod\n", + "In this tutorial, you will train a word2vec model in TensorFlow using distributed training via [Horovod](https://github.com/uber/horovod)." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Prerequisites\n", + "* Understand the [architecture and terms](https://docs.microsoft.com/azure/machine-learning/service/concept-azure-machine-learning-architecture) introduced by Azure Machine Learning (AML)\n", + "* Go through the [00.configuration.ipynb](https://github.com/Azure/MachineLearningNotebooks/blob/master/00.configuration.ipynb) notebook to:\n", + " * install the AML SDK\n", + " * create a workspace and its configuration file (`config.json`)\n", + "* Review the [tutorial](https://aka.ms/aml-notebook-hyperdrive) on single-node TensorFlow training using the SDK" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Check core SDK version number\n", + "import azureml.core\n", + "\n", + "print(\"SDK version:\", azureml.core.VERSION)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Initialize workspace\n", + "Initialize a [Workspace](https://docs.microsoft.com/azure/machine-learning/service/concept-azure-machine-learning-architecture#workspace) object from the existing workspace you created in the Prerequisites step. `Workspace.from_config()` creates a workspace object from the details stored in `config.json`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from azureml.core.workspace import Workspace\n", + "\n", + "ws = Workspace.from_config()\n", + "print('Workspace name: ' + ws.name, \n", + " 'Azure region: ' + ws.location, \n", + " 'Subscription id: ' + ws.subscription_id, \n", + " 'Resource group: ' + ws.resource_group, sep = '\\n')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Create a remote compute target\n", + "You will need to create a [compute target](https://docs.microsoft.com/azure/machine-learning/service/concept-azure-machine-learning-architecture#compute-target) to execute your training script on. In this tutorial, you create an [Azure Batch AI](https://docs.microsoft.com/azure/batch-ai/overview) cluster as your training compute resource. This code creates a cluster for you if it does not already exist in your workspace.\n", + "\n", + "**Creation of the cluster takes approximately 5 minutes.** If the cluster is already in your workspace this code will skip the cluster creation process." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from azureml.core.compute import ComputeTarget, BatchAiCompute\n", + "from azureml.core.compute_target import ComputeTargetException\n", + "\n", + "# choose a name for your cluster\n", + "cluster_name = \"gpucluster\"\n", + "\n", + "try:\n", + " compute_target = ComputeTarget(workspace=ws, name=cluster_name)\n", + " print('Found existing compute target')\n", + "except ComputeTargetException:\n", + " print('Creating a new compute target...')\n", + " compute_config = BatchAiCompute.provisioning_configuration(vm_size='STANDARD_NC6', \n", + " autoscale_enabled=True,\n", + " cluster_min_nodes=0, \n", + " cluster_max_nodes=4)\n", + "\n", + " # create the cluster\n", + " compute_target = ComputeTarget.create(ws, cluster_name, compute_config)\n", + "\n", + " compute_target.wait_for_completion(show_output=True)\n", + "\n", + " # Use the 'status' property to get a detailed status for the current cluster. \n", + " print(compute_target.status.serialize())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The above code creates a GPU cluster. If you instead want to create a CPU cluster, provide a different VM size to the `vm_size` parameter, such as `STANDARD_D2_V2`." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Upload data to datastore\n", + "To make data accessible for remote training, AML provides a convenient way to do so via a [Datastore](https://docs.microsoft.com/azure/machine-learning/service/how-to-access-data). The datastore provides a mechanism for you to upload/download data to Azure Storage, and interact with it from your remote compute targets. \n", + "\n", + "If your data is already stored in Azure, or you download the data as part of your training script, you will not need to do this step. For this tutorial, although you can download the data in your training script, we will demonstrate how to upload the training data to a datastore and access it during training to illustrate the datastore functionality." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "First, download the training data from [here](http://mattmahoney.net/dc/text8.zip) to your local machine:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import urllib\n", + "\n", + "os.makedirs('./data', exist_ok=True)\n", + "download_url = 'http://mattmahoney.net/dc/text8.zip'\n", + "urllib.request.urlretrieve(download_url, filename='./data/text8.zip')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Each workspace is associated with a default datastore. In this tutorial, we will upload the training data to this default datastore. The below code will upload the contents of the data directory to the path `./data` on the default datastore." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "ds = ws.get_default_datastore()\n", + "print(ds.datastore_type, ds.account_name, ds.container_name)\n", + "\n", + "ds.upload(src_dir='data', target_path='data', overwrite=True, show_progress=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For convenience, let's get a reference to the path on the datastore with the zip file of training data. We can do so using the `path` method. In the next section, we can then pass this reference to our training script's `--input_data` argument. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "path_on_datastore = 'data/text8.zip'\n", + "ds_data = ds.path(path_on_datastore)\n", + "print(ds_data)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Train model on the remote compute" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Create a project directory\n", + "Create a directory that will contain all the necessary code from your local machine that you will need access to on the remote resource. This includes the training script, and any additional files your training script depends on." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "project_folder = './tf-distr-hvd'\n", + "os.makedirs(project_folder, exist_ok=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Copy the training script `tf_horovod_word2vec.py` into this project directory." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import shutil\n", + "shutil.copy('tf_horovod_word2vec.py', project_folder)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Create an experiment\n", + "Create an [Experiment](https://docs.microsoft.com/azure/machine-learning/service/concept-azure-machine-learning-architecture#experiment) to track all the runs in your workspace for this distributed TensorFlow tutorial. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from azureml.core import Experiment\n", + "\n", + "experiment_name = 'tf-distr-hvd'\n", + "experiment = Experiment(ws, name=experiment_name)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Create a TensorFlow estimator\n", + "The AML SDK's TensorFlow estimator enables you to easily submit TensorFlow training jobs for both single-node and distributed runs. For more information on the TensorFlow estimator, refer [here](https://docs.microsoft.com/azure/machine-learning/service/how-to-train-tensorflow)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from azureml.train.dnn import TensorFlow\n", + "\n", + "script_params={\n", + " '--input_data': ds_data\n", + "}\n", + "\n", + "estimator= TensorFlow(source_directory=project_folder,\n", + " compute_target=compute_target,\n", + " script_params=script_params,\n", + " entry_script='tf_horovod_word2vec.py',\n", + " node_count=2,\n", + " process_count_per_node=1,\n", + " distributed_backend='mpi',\n", + " use_gpu=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The above code specifies that we will run our training script on `2` nodes, with one worker per node. In order to execute a distributed run using MPI/Horovod, you must provide the argument `distributed_backend='mpi'`. Using this estimator with these settings, TensorFlow, Horovod and their dependencies will be installed for you. However, if your script also uses other packages, make sure to install them via the `TensorFlow` constructor's `pip_packages` or `conda_packages` parameters.\n", + "\n", + "Note that we passed our training data reference `ds_data` to our script's `--input_data` argument. This will 1) mount our datastore on the remote compute and 2) provide the path to the data zip file on our datastore." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Submit job\n", + "Run your experiment by submitting your estimator object. Note that this call is asynchronous." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "run = experiment.submit(estimator)\n", + "print(run)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Monitor your run\n", + "You can monitor the progress of the run with a Jupyter widget. Like the run submission, the widget is asynchronous and provides live updates every 10-15 seconds until the job completes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from azureml.train.widgets import RunDetails\n", + "RunDetails(run).show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Alternatively, you can block until the script has completed training before running more code." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "run.wait_for_completion(show_output=True)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python [default]", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.6" + }, + "msauthor": "minxia" + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/training/04.distributed-tensorflow-with-horovod/tf_horovod_word2vec.py b/training/04.distributed-tensorflow-with-horovod/tf_horovod_word2vec.py new file mode 100644 index 000000000..98c1e5ee7 --- /dev/null +++ b/training/04.distributed-tensorflow-with-horovod/tf_horovod_word2vec.py @@ -0,0 +1,259 @@ +# Copyright 2015 The TensorFlow Authors. All Rights Reserved. +# Modifications copyright (C) 2017 Uber Technologies, Inc. +# Additional modifications copyright (C) Microsoft Corporation +# Licensed under the Apache License, Version 2.0 +# Script adapted from: https://github.com/uber/horovod/blob/master/examples/tensorflow_word2vec.py +# ====================================== +"""Basic word2vec example.""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import collections +import math +import os +import random +import zipfile +import argparse + +import numpy as np +from six.moves import urllib +from six.moves import xrange # pylint: disable=redefined-builtin +import tensorflow as tf +import horovod.tensorflow as hvd +from azureml.core.run import Run + +# Horovod: initialize Horovod. +hvd.init() + +parser = argparse.ArgumentParser() +parser.add_argument('--input_data', type=str, help='training data') + +args = parser.parse_args() + +input_data = args.input_data +print("the input data is at %s" % input_data) + +# Step 1: Download the data. +url = 'http://mattmahoney.net/dc/text8.zip' + + +def maybe_download(filename, expected_bytes): + """Download a file if not present, and make sure it's the right size.""" + if not filename: + filename = "text8.zip" + if not os.path.exists(filename): + print("Downloading the data from http://mattmahoney.net/dc/text8.zip") + filename, _ = urllib.request.urlretrieve(url, filename) + else: + print("Use the data from %s" % input_data) + statinfo = os.stat(filename) + if statinfo.st_size == expected_bytes: + print('Found and verified', filename) + else: + print(statinfo.st_size) + raise Exception( + 'Failed to verify ' + url + '. Can you get to it with a browser?') + return filename + + +filename = maybe_download(input_data, 31344016) + + +# Read the data into a list of strings. +def read_data(filename): + """Extract the first file enclosed in a zip file as a list of words.""" + with zipfile.ZipFile(filename) as f: + data = tf.compat.as_str(f.read(f.namelist()[0])).split() + return data + + +vocabulary = read_data(filename) +print('Data size', len(vocabulary)) + +# Step 2: Build the dictionary and replace rare words with UNK token. +vocabulary_size = 50000 + + +def build_dataset(words, n_words): + """Process raw inputs into a dataset.""" + count = [['UNK', -1]] + count.extend(collections.Counter(words).most_common(n_words - 1)) + dictionary = dict() + for word, _ in count: + dictionary[word] = len(dictionary) + data = list() + unk_count = 0 + for word in words: + if word in dictionary: + index = dictionary[word] + else: + index = 0 # dictionary['UNK'] + unk_count += 1 + data.append(index) + count[0][1] = unk_count + reversed_dictionary = dict(zip(dictionary.values(), dictionary.keys())) + return data, count, dictionary, reversed_dictionary + + +data, count, dictionary, reverse_dictionary = build_dataset(vocabulary, + vocabulary_size) +del vocabulary # Hint to reduce memory. +print('Most common words (+UNK)', count[:5]) +print('Sample data', data[:10], [reverse_dictionary[i] for i in data[:10]]) + + +# Step 3: Function to generate a training batch for the skip-gram model. +def generate_batch(batch_size, num_skips, skip_window): + assert num_skips <= 2 * skip_window + # Adjust batch_size to match num_skips + batch_size = batch_size // num_skips * num_skips + span = 2 * skip_window + 1 # [ skip_window target skip_window ] + # Backtrack a little bit to avoid skipping words in the end of a batch + data_index = random.randint(0, len(data) - span - 1) + batch = np.ndarray(shape=(batch_size), dtype=np.int32) + labels = np.ndarray(shape=(batch_size, 1), dtype=np.int32) + buffer = collections.deque(maxlen=span) + for _ in range(span): + buffer.append(data[data_index]) + data_index = (data_index + 1) % len(data) + for i in range(batch_size // num_skips): + target = skip_window # target label at the center of the buffer + targets_to_avoid = [skip_window] + for j in range(num_skips): + while target in targets_to_avoid: + target = random.randint(0, span - 1) + targets_to_avoid.append(target) + batch[i * num_skips + j] = buffer[skip_window] + labels[i * num_skips + j, 0] = buffer[target] + buffer.append(data[data_index]) + data_index = (data_index + 1) % len(data) + return batch, labels + + +batch, labels = generate_batch(batch_size=8, num_skips=2, skip_window=1) +for i in range(8): + print(batch[i], reverse_dictionary[batch[i]], + '->', labels[i, 0], reverse_dictionary[labels[i, 0]]) + +# Step 4: Build and train a skip-gram model. + +max_batch_size = 128 +embedding_size = 128 # Dimension of the embedding vector. +skip_window = 1 # How many words to consider left and right. +num_skips = 2 # How many times to reuse an input to generate a label. + +# We pick a random validation set to sample nearest neighbors. Here we limit the +# validation samples to the words that have a low numeric ID, which by +# construction are also the most frequent. +valid_size = 16 # Random set of words to evaluate similarity on. +valid_window = 100 # Only pick dev samples in the head of the distribution. +valid_examples = np.random.choice(valid_window, valid_size, replace=False) +num_sampled = 64 # Number of negative examples to sample. + +graph = tf.Graph() + +with graph.as_default(): + + # Input data. + train_inputs = tf.placeholder(tf.int32, shape=[None]) + train_labels = tf.placeholder(tf.int32, shape=[None, 1]) + valid_dataset = tf.constant(valid_examples, dtype=tf.int32) + + # Look up embeddings for inputs. + embeddings = tf.Variable( + tf.random_uniform([vocabulary_size, embedding_size], -1.0, 1.0)) + embed = tf.nn.embedding_lookup(embeddings, train_inputs) + + # Construct the variables for the NCE loss + nce_weights = tf.Variable( + tf.truncated_normal([vocabulary_size, embedding_size], + stddev=1.0 / math.sqrt(embedding_size))) + nce_biases = tf.Variable(tf.zeros([vocabulary_size])) + + # Compute the average NCE loss for the batch. + # tf.nce_loss automatically draws a new sample of the negative labels each + # time we evaluate the loss. + loss = tf.reduce_mean( + tf.nn.nce_loss(weights=nce_weights, + biases=nce_biases, + labels=train_labels, + inputs=embed, + num_sampled=num_sampled, + num_classes=vocabulary_size)) + + # Horovod: adjust learning rate based on number of GPUs. + optimizer = tf.train.GradientDescentOptimizer(1.0 * hvd.size()) + + # Horovod: add Horovod Distributed Optimizer. + optimizer = hvd.DistributedOptimizer(optimizer) + + train_op = optimizer.minimize(loss) + + # Compute the cosine similarity between minibatch examples and all embeddings. + norm = tf.sqrt(tf.reduce_sum(tf.square(embeddings), 1, keep_dims=True)) + normalized_embeddings = embeddings / norm + valid_embeddings = tf.nn.embedding_lookup( + normalized_embeddings, valid_dataset) + similarity = tf.matmul( + valid_embeddings, normalized_embeddings, transpose_b=True) + + # Add variable initializer. + init = tf.global_variables_initializer() + + # Horovod: broadcast initial variable states from rank 0 to all other processes. + # This is necessary to ensure consistent initialization of all workers when + # training is started with random weights or restored from a checkpoint. + bcast = hvd.broadcast_global_variables(0) + +# Step 5: Begin training. + +# Horovod: adjust number of steps based on number of GPUs. +num_steps = 4000 // hvd.size() + 1 + +# Horovod: pin GPU to be used to process local rank (one GPU per process) +config = tf.ConfigProto() +config.gpu_options.allow_growth = True +config.gpu_options.visible_device_list = str(hvd.local_rank()) + +with tf.Session(graph=graph, config=config) as session: + # We must initialize all variables before we use them. + init.run() + bcast.run() + print('Initialized') + run = Run.get_submitted_run() + average_loss = 0 + for step in xrange(num_steps): + # simulate various sentence length by randomization + batch_size = random.randint(max_batch_size // 2, max_batch_size) + batch_inputs, batch_labels = generate_batch( + batch_size, num_skips, skip_window) + feed_dict = {train_inputs: batch_inputs, train_labels: batch_labels} + + # We perform one update step by evaluating the optimizer op (including it + # in the list of returned values for session.run() + _, loss_val = session.run([train_op, loss], feed_dict=feed_dict) + average_loss += loss_val + + if step % 2000 == 0: + if step > 0: + average_loss /= 2000 + # The average loss is an estimate of the loss over the last 2000 batches. + print('Average loss at step ', step, ': ', average_loss) + run.log("Loss", average_loss) + average_loss = 0 + final_embeddings = normalized_embeddings.eval() + + # Evaluate similarity in the end on worker 0. + if hvd.rank() == 0: + sim = similarity.eval() + for i in xrange(valid_size): + valid_word = reverse_dictionary[valid_examples[i]] + top_k = 8 # number of nearest neighbors + nearest = (-sim[i, :]).argsort()[1:top_k + 1] + log_str = 'Nearest to %s:' % valid_word + for k in xrange(top_k): + close_word = reverse_dictionary[nearest[k]] + log_str = '%s %s,' % (log_str, close_word) + print(log_str) diff --git a/training/05.distributed-tensorflow-with-parameter-server/05.distributed-tensorflow-with-parameter-server.ipynb b/training/05.distributed-tensorflow-with-parameter-server/05.distributed-tensorflow-with-parameter-server.ipynb new file mode 100644 index 000000000..92daf0938 --- /dev/null +++ b/training/05.distributed-tensorflow-with-parameter-server/05.distributed-tensorflow-with-parameter-server.ipynb @@ -0,0 +1,286 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Copyright (c) Microsoft Corporation. All rights reserved.\n", + "\n", + "Licensed under the MIT License." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 05. Distributed TensorFlow with parameter server\n", + "In this tutorial, you will train a TensorFlow model on the [MNIST](http://yann.lecun.com/exdb/mnist/) dataset using native [distributed TensorFlow](https://www.tensorflow.org/deploy/distributed)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Prerequisites\n", + "* Understand the [architecture and terms](https://docs.microsoft.com/azure/machine-learning/service/concept-azure-machine-learning-architecture) introduced by Azure Machine Learning (AML)\n", + "* Go through the [00.configuration.ipynb](https://github.com/Azure/MachineLearningNotebooks/blob/master/00.configuration.ipynb) notebook to:\n", + " * install the AML SDK\n", + " * create a workspace and its configuration file (`config.json`)\n", + "* Review the [tutorial](https://aka.ms/aml-notebook-hyperdrive) on single-node TensorFlow training using the SDK" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Check core SDK version number\n", + "import azureml.core\n", + "\n", + "print(\"SDK version:\", azureml.core.VERSION)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Initialize workspace\n", + "Initialize a [Workspace](https://docs.microsoft.com/azure/machine-learning/service/concept-azure-machine-learning-architecture#workspace) object from the existing workspace you created in the Prerequisites step. `Workspace.from_config()` creates a workspace object from the details stored in `config.json`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from azureml.core.workspace import Workspace\n", + "\n", + "ws = Workspace.from_config()\n", + "print('Workspace name: ' + ws.name, \n", + " 'Azure region: ' + ws.location, \n", + " 'Subscription id: ' + ws.subscription_id, \n", + " 'Resource group: ' + ws.resource_group, sep = '\\n')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Create a remote compute target\n", + "You will need to create a [compute target](https://docs.microsoft.com/azure/machine-learning/service/concept-azure-machine-learning-architecture#compute-target) to execute your training script on. In this tutorial, you create an [Azure Batch AI](https://docs.microsoft.com/azure/batch-ai/overview) cluster as your training compute resource. This code creates a cluster for you if it does not already exist in your workspace.\n", + "\n", + "**Creation of the cluster takes approximately 5 minutes.** If the cluster is already in your workspace this code will skip the cluster creation process." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from azureml.core.compute import ComputeTarget, BatchAiCompute\n", + "from azureml.core.compute_target import ComputeTargetException\n", + "\n", + "# choose a name for your cluster\n", + "cluster_name = \"gpucluster\"\n", + "\n", + "try:\n", + " compute_target = ComputeTarget(workspace=ws, name=cluster_name)\n", + " print('Found existing compute target.')\n", + "except ComputeTargetException:\n", + " print('Creating a new compute target...')\n", + " compute_config = BatchAiCompute.provisioning_configuration(vm_size='STANDARD_NC6', \n", + " autoscale_enabled=True,\n", + " cluster_min_nodes=0, \n", + " cluster_max_nodes=4)\n", + "\n", + " # create the cluster\n", + " compute_target = ComputeTarget.create(ws, cluster_name, compute_config)\n", + "\n", + " compute_target.wait_for_completion(show_output=True)\n", + "\n", + " # Use the 'status' property to get a detailed status for the current cluster. \n", + " print(compute_target.status.serialize())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Train model on the remote compute\n", + "Now that we have the cluster ready to go, let's run our distributed training job." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Create a project directory\n", + "Create a directory that will contain all the necessary code from your local machine that you will need access to on the remote resource. This includes the training script, and any additional files your training script depends on." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "\n", + "project_folder = './tf-distr-ps'\n", + "os.makedirs(project_folder, exist_ok=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Copy the training script `tf_mnist_replica.py` into this project directory." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import shutil\n", + "shutil.copy('tf_mnist_replica.py', project_folder)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Create an experiment\n", + "Create an [Experiment](https://docs.microsoft.com/azure/machine-learning/service/concept-azure-machine-learning-architecture#experiment) to track all the runs in your workspace for this distributed TensorFlow tutorial. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from azureml.core import Experiment\n", + "\n", + "experiment_name = 'tf-distr-ps'\n", + "experiment = Experiment(ws, name=experiment_name)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Create a TensorFlow estimator\n", + "The AML SDK's TensorFlow estimator enables you to easily submit TensorFlow training jobs for both single-node and distributed runs. For more information on the TensorFlow estimator, refer [here](https://docs.microsoft.com/azure/machine-learning/service/how-to-train-tensorflow)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from azureml.train.dnn import TensorFlow\n", + "\n", + "script_params={\n", + " '--num_gpus': 1\n", + "}\n", + "\n", + "estimator = TensorFlow(source_directory=project_folder,\n", + " compute_target=compute_target,\n", + " script_params=script_params,\n", + " entry_script='tf_mnist_replica.py',\n", + " node_count=2,\n", + " worker_count=2,\n", + " parameter_server_count=1, \n", + " distributed_backend='ps',\n", + " use_gpu=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The above code specifies that we will run our training script on `2` nodes, with two workers and one parameter server. In order to execute a native distributed TensorFlow run, you must provide the argument `distributed_backend='ps'`. Using this estimator with these settings, TensorFlow and its dependencies will be installed for you. However, if your script also uses other packages, make sure to install them via the `TensorFlow` constructor's `pip_packages` or `conda_packages` parameters." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Submit job\n", + "Run your experiment by submitting your estimator object. Note that this call is asynchronous." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "run = experiment.submit(estimator)\n", + "print(run.get_details())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Monitor your run\n", + "You can monitor the progress of the run with a Jupyter widget. Like the run submission, the widget is asynchronous and provides live updates every 10-15 seconds until the job completes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from azureml.train.widgets import RunDetails\n", + "RunDetails(run).show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Alternatively, you can block until the script has completed training before running more code." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "run.wait_for_completion(show_output=True) # this provides a verbose log" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python [default]", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.6" + }, + "msauthor": "minxia" + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/training/05.distributed-tensorflow-with-parameter-server/tf_mnist_replica.py b/training/05.distributed-tensorflow-with-parameter-server/tf_mnist_replica.py new file mode 100644 index 000000000..1476dd5bc --- /dev/null +++ b/training/05.distributed-tensorflow-with-parameter-server/tf_mnist_replica.py @@ -0,0 +1,271 @@ +# Copyright 2016 The TensorFlow Authors. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 +# Script adapted from: +# https://github.com/tensorflow/tensorflow/blob/master/tensorflow/tools/dist_test/python/mnist_replica.py +# ============================================================================== +"""Distributed MNIST training and validation, with model replicas. +A simple softmax model with one hidden layer is defined. The parameters +(weights and biases) are located on one parameter server (ps), while the ops +are executed on two worker nodes by default. The TF sessions also run on the +worker node. +Multiple invocations of this script can be done in parallel, with different +values for --task_index. There should be exactly one invocation with +--task_index, which will create a master session that carries out variable +initialization. The other, non-master, sessions will wait for the master +session to finish the initialization before proceeding to the training stage. +The coordination between the multiple worker invocations occurs due to +the definition of the parameters on the same ps devices. The parameter updates +from one worker is visible to all other workers. As such, the workers can +perform forward computation and gradient calculation in parallel, which +should lead to increased training speed for the simple model. +""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import os +import math +import sys +import tempfile +import time +import json + +import tensorflow as tf +from tensorflow.examples.tutorials.mnist import input_data +from azureml.core.run import Run + +flags = tf.app.flags +flags.DEFINE_string("data_dir", "/tmp/mnist-data", + "Directory for storing mnist data") +flags.DEFINE_boolean("download_only", False, + "Only perform downloading of data; Do not proceed to " + "session preparation, model definition or training") +flags.DEFINE_integer("num_gpus", 0, "Total number of gpus for each machine." + "If you don't use GPU, please set it to '0'") +flags.DEFINE_integer("replicas_to_aggregate", None, + "Number of replicas to aggregate before parameter update " + "is applied (For sync_replicas mode only; default: " + "num_workers)") +flags.DEFINE_integer("hidden_units", 100, + "Number of units in the hidden layer of the NN") +flags.DEFINE_integer("train_steps", 200, + "Number of (global) training steps to perform") +flags.DEFINE_integer("batch_size", 100, "Training batch size") +flags.DEFINE_float("learning_rate", 0.01, "Learning rate") +flags.DEFINE_boolean( + "sync_replicas", False, + "Use the sync_replicas (synchronized replicas) mode, " + "wherein the parameter updates from workers are aggregated " + "before applied to avoid stale gradients") +flags.DEFINE_boolean( + "existing_servers", False, "Whether servers already exists. If True, " + "will use the worker hosts via their GRPC URLs (one client process " + "per worker host). Otherwise, will create an in-process TensorFlow " + "server.") + +FLAGS = flags.FLAGS + +IMAGE_PIXELS = 28 + + +def main(unused_argv): + data_root = os.path.join("outputs", "MNIST") + mnist = None + tf_config = os.environ.get("TF_CONFIG") + if not tf_config or tf_config == "": + raise ValueError("TF_CONFIG not found.") + tf_config_json = json.loads(tf_config) + cluster = tf_config_json.get('cluster') + job_name = tf_config_json.get('task', {}).get('type') + task_index = tf_config_json.get('task', {}).get('index') + job_name = "worker" if job_name == "master" else job_name + sentinel_path = os.path.join(data_root, "complete.txt") + if job_name == "worker" and task_index == 0: + mnist = input_data.read_data_sets(data_root, one_hot=True) + with open(sentinel_path, 'w+') as f: + f.write("download complete") + else: + while not os.path.exists(sentinel_path): + time.sleep(0.01) + mnist = input_data.read_data_sets(data_root, one_hot=True) + + if FLAGS.download_only: + sys.exit(0) + + print("job name = %s" % job_name) + print("task index = %d" % task_index) + print("number of GPUs = %d" % FLAGS.num_gpus) + + # Construct the cluster and start the server + cluster_spec = tf.train.ClusterSpec(cluster) + + # Get the number of workers. + num_workers = len(cluster_spec.task_indices("worker")) + + if not FLAGS.existing_servers: + # Not using existing servers. Create an in-process server. + server = tf.train.Server( + cluster_spec, job_name=job_name, task_index=task_index) + if job_name == "ps": + server.join() + + is_chief = (task_index == 0) + if FLAGS.num_gpus > 0: + # Avoid gpu allocation conflict: now allocate task_num -> #gpu + # for each worker in the corresponding machine + gpu = (task_index % FLAGS.num_gpus) + worker_device = "/job:worker/task:%d/gpu:%d" % (task_index, gpu) + elif FLAGS.num_gpus == 0: + # Just allocate the CPU to worker server + cpu = 0 + worker_device = "/job:worker/task:%d/cpu:%d" % (task_index, cpu) + # The device setter will automatically place Variables ops on separate + # parameter servers (ps). The non-Variable ops will be placed on the workers. + # The ps use CPU and workers use corresponding GPU + with tf.device( + tf.train.replica_device_setter( + worker_device=worker_device, + ps_device="/job:ps/cpu:0", + cluster=cluster)): + global_step = tf.Variable(0, name="global_step", trainable=False) + + # Variables of the hidden layer + hid_w = tf.Variable( + tf.truncated_normal( + [IMAGE_PIXELS * IMAGE_PIXELS, FLAGS.hidden_units], + stddev=1.0 / IMAGE_PIXELS), + name="hid_w") + hid_b = tf.Variable(tf.zeros([FLAGS.hidden_units]), name="hid_b") + + # Variables of the softmax layer + sm_w = tf.Variable( + tf.truncated_normal( + [FLAGS.hidden_units, 10], + stddev=1.0 / math.sqrt(FLAGS.hidden_units)), + name="sm_w") + sm_b = tf.Variable(tf.zeros([10]), name="sm_b") + + # Ops: located on the worker specified with task_index + x = tf.placeholder(tf.float32, [None, IMAGE_PIXELS * IMAGE_PIXELS]) + y_ = tf.placeholder(tf.float32, [None, 10]) + + hid_lin = tf.nn.xw_plus_b(x, hid_w, hid_b) + hid = tf.nn.relu(hid_lin) + + y = tf.nn.softmax(tf.nn.xw_plus_b(hid, sm_w, sm_b)) + cross_entropy = -tf.reduce_sum(y_ * tf.log(tf.clip_by_value(y, 1e-10, 1.0))) + + opt = tf.train.AdamOptimizer(FLAGS.learning_rate) + + if FLAGS.sync_replicas: + if FLAGS.replicas_to_aggregate is None: + replicas_to_aggregate = num_workers + else: + replicas_to_aggregate = FLAGS.replicas_to_aggregate + + opt = tf.train.SyncReplicasOptimizer( + opt, + replicas_to_aggregate=replicas_to_aggregate, + total_num_replicas=num_workers, + name="mnist_sync_replicas") + + train_step = opt.minimize(cross_entropy, global_step=global_step) + + if FLAGS.sync_replicas: + local_init_op = opt.local_step_init_op + if is_chief: + local_init_op = opt.chief_init_op + + ready_for_local_init_op = opt.ready_for_local_init_op + + # Initial token and chief queue runners required by the sync_replicas mode + chief_queue_runner = opt.get_chief_queue_runner() + sync_init_op = opt.get_init_tokens_op() + + init_op = tf.global_variables_initializer() + train_dir = tempfile.mkdtemp() + + if FLAGS.sync_replicas: + sv = tf.train.Supervisor( + is_chief=is_chief, + logdir=train_dir, + init_op=init_op, + local_init_op=local_init_op, + ready_for_local_init_op=ready_for_local_init_op, + recovery_wait_secs=1, + global_step=global_step) + else: + sv = tf.train.Supervisor( + is_chief=is_chief, + logdir=train_dir, + init_op=init_op, + recovery_wait_secs=1, + global_step=global_step) + + sess_config = tf.ConfigProto( + allow_soft_placement=True, + log_device_placement=False, + device_filters=["/job:ps", + "/job:worker/task:%d" % task_index]) + + # The chief worker (task_index==0) session will prepare the session, + # while the remaining workers will wait for the preparation to complete. + if is_chief: + print("Worker %d: Initializing session..." % task_index) + else: + print("Worker %d: Waiting for session to be initialized..." % + task_index) + + if FLAGS.existing_servers: + server_grpc_url = "grpc://" + task_index + print("Using existing server at: %s" % server_grpc_url) + + sess = sv.prepare_or_wait_for_session(server_grpc_url, config=sess_config) + else: + sess = sv.prepare_or_wait_for_session(server.target, config=sess_config) + + print("Worker %d: Session initialization complete." % task_index) + + if FLAGS.sync_replicas and is_chief: + # Chief worker will start the chief queue runner and call the init op. + sess.run(sync_init_op) + sv.start_queue_runners(sess, [chief_queue_runner]) + + # Perform training + time_begin = time.time() + print("Training begins @ %f" % time_begin) + + local_step = 0 + while True: + # Training feed + batch_xs, batch_ys = mnist.train.next_batch(FLAGS.batch_size) + train_feed = {x: batch_xs, y_: batch_ys} + + _, step = sess.run([train_step, global_step], feed_dict=train_feed) + local_step += 1 + + now = time.time() + print("%f: Worker %d: training step %d done (global step: %d)" % + (now, task_index, local_step, step)) + + if step >= FLAGS.train_steps: + break + + time_end = time.time() + print("Training ends @ %f" % time_end) + training_time = time_end - time_begin + print("Training elapsed time: %f s" % training_time) + + # Validation feed + val_feed = {x: mnist.validation.images, y_: mnist.validation.labels} + val_xent = sess.run(cross_entropy, feed_dict=val_feed) + print("After %d training step(s), validation cross entropy = %g" % + (FLAGS.train_steps, val_xent)) + if job_name == "worker" and task_index == 0: + run = Run.get_submitted_run() + run.log("CrossEntropy", val_xent) + + +if __name__ == "__main__": + tf.app.run() diff --git a/training/06.distributed-cntk-with-custom-docker/06.distributed-cntk-with-custom-docker.ipynb b/training/06.distributed-cntk-with-custom-docker/06.distributed-cntk-with-custom-docker.ipynb new file mode 100644 index 000000000..de9a0d409 --- /dev/null +++ b/training/06.distributed-cntk-with-custom-docker/06.distributed-cntk-with-custom-docker.ipynb @@ -0,0 +1,283 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Copyright (c) Microsoft Corporation. All rights reserved.\n", + "\n", + "Licensed under the MIT License." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 06. Distributed CNTK using custom docker images\n", + "In this tutorial, you will train a CNTK model on the [MNIST](http://yann.lecun.com/exdb/mnist/) dataset using a custom docker image and distributed training." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Prerequisites\n", + "* Understand the [architecture and terms](https://docs.microsoft.com/azure/machine-learning/service/concept-azure-machine-learning-architecture) introduced by Azure Machine Learning services\n", + "* Go through the [00.configuration.ipynb]() notebook to:\n", + " * install the AML SDK\n", + " * create a workspace and its configuration file (`config.json`)\n", + "* Review the [tutorial]() on single-node PyTorch training using the SDK" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Check core SDK version number\n", + "import azureml.core\n", + "\n", + "print(\"SDK version:\", azureml.core.VERSION)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Initialize workspace\n", + "\n", + "Initialize a [Workspace](https://review.docs.microsoft.com/en-us/azure/machine-learning/service/concept-azure-machine-learning-architecture?branch=release-ignite-aml#workspace) object from the existing workspace you created in the Prerequisites step. `Workspace.from_config()` creates a workspace object from the details stored in `config.json`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from azureml.core.workspace import Workspace\n", + "\n", + "ws = Workspace.from_config()\n", + "print('Workspace name: ' + ws.name, \n", + " 'Azure region: ' + ws.location, \n", + " 'Subscription id: ' + ws.subscription_id, \n", + " 'Resource group: ' + ws.resource_group, sep = '\\n')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Create a remote compute target\n", + "You will need to create a [compute target](https://docs.microsoft.com/azure/machine-learning/service/concept-azure-machine-learning-architecture#compute-target) to execute your training script on. In this tutorial, you create an [Azure Batch AI](https://docs.microsoft.com/azure/batch-ai/overview) cluster as your training compute resource. This code creates a cluster for you if it does not already exist in your workspace.\n", + "\n", + "**Creation of the cluster takes approximately 5 minutes.** If the cluster is already in your workspace this code will skip the cluster creation process." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from azureml.core.compute import ComputeTarget, BatchAiCompute\n", + "from azureml.core.compute_target import ComputeTargetException\n", + "\n", + "# choose a name for your cluster\n", + "cluster_name = \"gpucluster\"\n", + "\n", + "try:\n", + " compute_target = ComputeTarget(workspace=ws, name=cluster_name)\n", + " print('Found existing compute target.')\n", + "except ComputeTargetException:\n", + " print('Creating a new compute target...')\n", + " compute_config = BatchAiCompute.provisioning_configuration(vm_size='STANDARD_NC6', \n", + " autoscale_enabled=True,\n", + " cluster_min_nodes=0, \n", + " cluster_max_nodes=4)\n", + "\n", + " # create the cluster\n", + " compute_target = ComputeTarget.create(ws, cluster_name, compute_config)\n", + "\n", + " compute_target.wait_for_completion(show_output=True)\n", + "\n", + " # Use the 'status' property to get a detailed status for the current cluster. \n", + " print(compute_target.status.serialize())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Train model on the remote compute\n", + "Now that we have the cluster ready to go, let's run our distributed training job." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Create a project directory\n", + "Create a directory that will contain all the necessary code from your local machine that you will need access to on the remote resource. This includes the training script, and any additional files your training script depends on." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "\n", + "project_folder = './cntk-distr'\n", + "os.makedirs(project_folder, exist_ok=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Copy the training script `tf_mnist_replica.py` into this project directory." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import shutil\n", + "shutil.copy('cntk_mnist.py', project_folder)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Create an experiment\n", + "Create an [experiment](https://docs.microsoft.com/azure/machine-learning/service/concept-azure-machine-learning-architecture#experiment) to track all the runs in your workspace for this distributed CNTK tutorial. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from azureml.core import Experiment\n", + "\n", + "experiment_name = 'cntk-distr'\n", + "experiment = Experiment(ws, name=experiment_name)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Create an Estimator\n", + "The AML SDK's base Estimator enables you to easily submit custom scripts for both single-node and distributed runs. You should this generic estimator for training code using frameworks such as sklearn or CNTK that don't have corresponding custom estimators. For more information on using the generic estimator, refer [here](https://docs.microsoft.com/azure/machine-learning/service/how-to-train-ml-models)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from azureml.train.estimator import *\n", + "\n", + "estimator = Estimator(source_directory=project_folder,\n", + " compute_target=compute_target,\n", + " entry_script='cntk_mnist.py',\n", + " node_count=2,\n", + " process_count_per_node=1,\n", + " distributed_backend='mpi', \n", + " pip_packages=['cntk==2.5.1'],\n", + " custom_docker_base_image='microsoft/mmlspark:0.12')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We would like to train our model using a [pre-built Docker container](https://hub.docker.com/r/microsoft/mmlspark/). To do so, we specify the name of the docker image to the argument `custom_docker_base_image`. You can only provide images available in public docker repositories such as Docker Hub using this argument. To use an image from a private docker repository, use the constructor's `environment_definition` parameter instead. Finally, we provide the `cntk` package to `pip_packages` to install CNTK 2.5.1 on our custom image.\n", + "\n", + "The above code specifies that we will run our training script on `2` nodes, with one worker per node. In order to run distributed CNTK, which uses MPI, you must provide the argument `distributed_backend='mpi'`." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Submit job\n", + "Run your experiment by submitting your estimator object. Note that this call is asynchronous." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "run = experiment.submit(estimator)\n", + "print(run.get_details())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Monitor your run\n", + "You can monitor the progress of the run with a Jupyter widget. Like the run submission, the widget is asynchronous and provides live updates every 10-15 seconds until the job completes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from azureml.train.widgets import RunDetails\n", + "RunDetails(run).show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Alternatively, you can block until the script has completed training before running more code." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "run.wait_for_completion(show_output=True)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python [default]", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.6" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/training/06.distributed-cntk-with-custom-docker/cntk_mnist.py b/training/06.distributed-cntk-with-custom-docker/cntk_mnist.py new file mode 100644 index 000000000..41ea88b2b --- /dev/null +++ b/training/06.distributed-cntk-with-custom-docker/cntk_mnist.py @@ -0,0 +1,321 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +# Script adapted from: +# 1. https://github.com/Microsoft/CNTK/blob/v2.0/Tutorials/CNTK_103A_MNIST_DataLoader.ipynb +# 2. https://github.com/Microsoft/CNTK/blob/v2.0/Tutorials/CNTK_103C_MNIST_MultiLayerPerceptron.ipynb +# =================================================================================================== +"""Train a CNTK multi-layer perceptron on the MNIST dataset.""" + +from __future__ import print_function +import gzip +import numpy as np +import os +import shutil +import struct +import sys +import time + +import cntk as C +from azureml.core.run import Run +import argparse + +run = Run.get_submitted_run() + +parser = argparse.ArgumentParser() + +parser.add_argument('--learning_rate', type=float, default=0.001, help='learning rate') +parser.add_argument('--num_hidden_layers', type=int, default=2, help='number of hidden layers') +parser.add_argument('--minibatch_size', type=int, default=64, help='minibatchsize') + +args = parser.parse_args() + +# Functions to load MNIST images and unpack into train and test set. +# - loadData reads image data and formats into a 28x28 long array +# - loadLabels reads the corresponding labels data, 1 for each image +# - load packs the downloaded image and labels data into a combined format to be read later by +# CNTK text reader + + +def loadData(src, cimg): + print('Downloading ' + src) + gzfname, h = urlretrieve(src, './delete.me') + print('Done.') + try: + with gzip.open(gzfname) as gz: + n = struct.unpack('I', gz.read(4)) + # Read magic number. + if n[0] != 0x3080000: + raise Exception('Invalid file: unexpected magic number.') + # Read number of entries. + n = struct.unpack('>I', gz.read(4))[0] + if n != cimg: + raise Exception('Invalid file: expected {0} entries.'.format(cimg)) + crow = struct.unpack('>I', gz.read(4))[0] + ccol = struct.unpack('>I', gz.read(4))[0] + if crow != 28 or ccol != 28: + raise Exception('Invalid file: expected 28 rows/cols per image.') + # Read data. + res = np.fromstring(gz.read(cimg * crow * ccol), dtype=np.uint8) + finally: + os.remove(gzfname) + return res.reshape((cimg, crow * ccol)) + + +def loadLabels(src, cimg): + print('Downloading ' + src) + gzfname, h = urlretrieve(src, './delete.me') + print('Done.') + try: + with gzip.open(gzfname) as gz: + n = struct.unpack('I', gz.read(4)) + # Read magic number. + if n[0] != 0x1080000: + raise Exception('Invalid file: unexpected magic number.') + # Read number of entries. + n = struct.unpack('>I', gz.read(4)) + if n[0] != cimg: + raise Exception('Invalid file: expected {0} rows.'.format(cimg)) + # Read labels. + res = np.fromstring(gz.read(cimg), dtype=np.uint8) + finally: + os.remove(gzfname) + return res.reshape((cimg, 1)) + + +def try_download(dataSrc, labelsSrc, cimg): + data = loadData(dataSrc, cimg) + labels = loadLabels(labelsSrc, cimg) + return np.hstack((data, labels)) + +# Save the data files into a format compatible with CNTK text reader + + +def savetxt(filename, ndarray): + dir = os.path.dirname(filename) + + if not os.path.exists(dir): + os.makedirs(dir) + + if not os.path.isfile(filename): + print("Saving", filename) + with open(filename, 'w') as f: + labels = list(map(' '.join, np.eye(10, dtype=np.uint).astype(str))) + for row in ndarray: + row_str = row.astype(str) + label_str = labels[row[-1]] + feature_str = ' '.join(row_str[:-1]) + f.write('|labels {} |features {}\n'.format(label_str, feature_str)) + else: + print("File already exists", filename) + +# Read a CTF formatted text (as mentioned above) using the CTF deserializer from a file + + +def create_reader(path, is_training, input_dim, num_label_classes): + return C.io.MinibatchSource(C.io.CTFDeserializer(path, C.io.StreamDefs( + labels=C.io.StreamDef(field='labels', shape=num_label_classes, is_sparse=False), + features=C.io.StreamDef(field='features', shape=input_dim, is_sparse=False) + )), randomize=is_training, max_sweeps=C.io.INFINITELY_REPEAT if is_training else 1) + +# Defines a utility that prints the training progress + + +def print_training_progress(trainer, mb, frequency, verbose=1): + training_loss = "NA" + eval_error = "NA" + + if mb % frequency == 0: + training_loss = trainer.previous_minibatch_loss_average + eval_error = trainer.previous_minibatch_evaluation_average + if verbose: + print("Minibatch: {0}, Loss: {1:.4f}, Error: {2:.2f}%".format(mb, training_loss, eval_error * 100)) + + return mb, training_loss, eval_error + +# Create the network architecture + + +def create_model(features): + with C.layers.default_options(init=C.layers.glorot_uniform(), activation=C.ops.relu): + h = features + for _ in range(num_hidden_layers): + h = C.layers.Dense(hidden_layers_dim)(h) + r = C.layers.Dense(num_output_classes, activation=None)(h) + return r + + +if __name__ == '__main__': + run = Run.get_submitted_run() + + try: + from urllib.request import urlretrieve + except ImportError: + from urllib import urlretrieve + + # Select the right target device when this script is being used: + if 'TEST_DEVICE' in os.environ: + if os.environ['TEST_DEVICE'] == 'cpu': + C.device.try_set_default_device(C.device.cpu()) + else: + C.device.try_set_default_device(C.device.gpu(0)) + + # URLs for the train image and labels data + url_train_image = 'http://yann.lecun.com/exdb/mnist/train-images-idx3-ubyte.gz' + url_train_labels = 'http://yann.lecun.com/exdb/mnist/train-labels-idx1-ubyte.gz' + num_train_samples = 60000 + + print("Downloading train data") + train = try_download(url_train_image, url_train_labels, num_train_samples) + + url_test_image = 'http://yann.lecun.com/exdb/mnist/t10k-images-idx3-ubyte.gz' + url_test_labels = 'http://yann.lecun.com/exdb/mnist/t10k-labels-idx1-ubyte.gz' + num_test_samples = 10000 + + print("Downloading test data") + test = try_download(url_test_image, url_test_labels, num_test_samples) + + # Save the train and test files (prefer our default path for the data + rank = os.environ.get("OMPI_COMM_WORLD_RANK") + data_dir = os.path.join("outputs", "MNIST") + sentinel_path = os.path.join(data_dir, "complete.txt") + if rank == '0': + print('Writing train text file...') + savetxt(os.path.join(data_dir, "Train-28x28_cntk_text.txt"), train) + + print('Writing test text file...') + savetxt(os.path.join(data_dir, "Test-28x28_cntk_text.txt"), test) + with open(sentinel_path, 'w+') as f: + f.write("download complete") + + print('Done with downloading data.') + else: + while not os.path.exists(sentinel_path): + time.sleep(0.01) + + # Ensure we always get the same amount of randomness + np.random.seed(0) + + # Define the data dimensions + input_dim = 784 + num_output_classes = 10 + + # Ensure the training and test data is generated and available for this tutorial. + # We search in two locations in the toolkit for the cached MNIST data set. + data_found = False + for data_dir in [os.path.join("..", "Examples", "Image", "DataSets", "MNIST"), + os.path.join("data_" + str(rank), "MNIST"), + os.path.join("outputs", "MNIST")]: + train_file = os.path.join(data_dir, "Train-28x28_cntk_text.txt") + test_file = os.path.join(data_dir, "Test-28x28_cntk_text.txt") + if os.path.isfile(train_file) and os.path.isfile(test_file): + data_found = True + break + if not data_found: + raise ValueError("Please generate the data by completing CNTK 103 Part A") + print("Data directory is {0}".format(data_dir)) + + num_hidden_layers = args.num_hidden_layers + hidden_layers_dim = 400 + + input = C.input_variable(input_dim) + label = C.input_variable(num_output_classes) + + z = create_model(input) + # Scale the input to 0-1 range by dividing each pixel by 255. + z = create_model(input / 255.0) + + loss = C.cross_entropy_with_softmax(z, label) + label_error = C.classification_error(z, label) + + # Instantiate the trainer object to drive the model training + learning_rate = args.learning_rate + lr_schedule = C.learning_rate_schedule(learning_rate, C.UnitType.minibatch) + learner = C.sgd(z.parameters, lr_schedule) + trainer = C.Trainer(z, (loss, label_error), [learner]) + + # Initialize the parameters for the trainer + minibatch_size = args.minibatch_size + num_samples_per_sweep = 60000 + num_sweeps_to_train_with = 10 + num_minibatches_to_train = (num_samples_per_sweep * num_sweeps_to_train_with) / minibatch_size + + # Create the reader to training data set + reader_train = create_reader(train_file, True, input_dim, num_output_classes) + + # Map the data streams to the input and labels. + input_map = { + label: reader_train.streams.labels, + input: reader_train.streams.features + } + + # Run the trainer on and perform model training + training_progress_output_freq = 500 + + errors = [] + losses = [] + for i in range(0, int(num_minibatches_to_train)): + # Read a mini batch from the training data file + data = reader_train.next_minibatch(minibatch_size, input_map=input_map) + + trainer.train_minibatch(data) + batchsize, loss, error = print_training_progress(trainer, i, training_progress_output_freq, verbose=1) + if (error != 'NA') and (loss != 'NA'): + errors.append(float(error)) + losses.append(float(loss)) + + # log the losses + if rank == '0': + run.log_list("Loss", losses) + run.log_list("Error", errors) + + # Read the training data + reader_test = create_reader(test_file, False, input_dim, num_output_classes) + + test_input_map = { + label: reader_test.streams.labels, + input: reader_test.streams.features, + } + + # Test data for trained model + test_minibatch_size = 512 + num_samples = 10000 + num_minibatches_to_test = num_samples // test_minibatch_size + test_result = 0.0 + + for i in range(num_minibatches_to_test): + # We are loading test data in batches specified by test_minibatch_size + # Each data point in the minibatch is a MNIST digit image of 784 dimensions + # with one pixel per dimension that we will encode / decode with the + # trained model. + data = reader_test.next_minibatch(test_minibatch_size, + input_map=test_input_map) + + eval_error = trainer.test_minibatch(data) + test_result = test_result + eval_error + + # Average of evaluation errors of all test minibatches + print("Average test error: {0:.2f}%".format((test_result * 100) / num_minibatches_to_test)) + + out = C.softmax(z) + + # Read the data for evaluation + reader_eval = create_reader(test_file, False, input_dim, num_output_classes) + + eval_minibatch_size = 25 + eval_input_map = {input: reader_eval.streams.features} + + data = reader_test.next_minibatch(eval_minibatch_size, input_map=test_input_map) + + img_label = data[label].asarray() + img_data = data[input].asarray() + predicted_label_prob = [out.eval(img_data[i]) for i in range(len(img_data))] + + # Find the index with the maximum value for both predicted as well as the ground truth + pred = [np.argmax(predicted_label_prob[i]) for i in range(len(predicted_label_prob))] + gtlabel = [np.argmax(img_label[i]) for i in range(len(img_label))] + + print("Label :", gtlabel[:25]) + print("Predicted:", pred) + + # save model to outputs folder + z.save('outputs/cntk.model') diff --git a/training/40.tensorboard/40.tensorboard.ipynb b/training/07.tensorboard/07.tensorboard.ipynb similarity index 99% rename from training/40.tensorboard/40.tensorboard.ipynb rename to training/07.tensorboard/07.tensorboard.ipynb index 97b64db9f..f34b5e652 100644 --- a/training/40.tensorboard/40.tensorboard.ipynb +++ b/training/07.tensorboard/07.tensorboard.ipynb @@ -480,7 +480,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python [default]", "language": "python", "name": "python3" }, @@ -494,7 +494,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.5" + "version": "3.6.6" } }, "nbformat": 4, diff --git a/training/41.export-run-history-to-tensorboard/41.export-run-history-to-tensorboard.ipynb b/training/08.export-run-history-to-tensorboard/08.export-run-history-to-tensorboard.ipynb similarity index 100% rename from training/41.export-run-history-to-tensorboard/41.export-run-history-to-tensorboard.ipynb rename to training/08.export-run-history-to-tensorboard/08.export-run-history-to-tensorboard.ipynb diff --git a/training/50.distributed-tensorflow-with-horovod/50.distributed-tensorflow-with-horovod.ipynb b/training/50.distributed-tensorflow-with-horovod/50.distributed-tensorflow-with-horovod.ipynb deleted file mode 100644 index a53acd5b3..000000000 --- a/training/50.distributed-tensorflow-with-horovod/50.distributed-tensorflow-with-horovod.ipynb +++ /dev/null @@ -1,500 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Copyright (c) Microsoft Corporation. All rights reserved.\n", - "\n", - "Licensed under the MIT License." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# 50. Distributed Tensorflow Horovod\n", - "\n", - "In this tutorial we demonstrate how to use the Azure ML Training SDK to train Tensorflow model in a distributed manner using Horovod framework.\n", - "\n", - "# Prerequisites\n", - "\n", - "Make sure you go through the [00. Installation and Configuration](00.configuration.ipynb) Notebook first if you haven't." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Check core SDK version number\n", - "import azureml.core\n", - "\n", - "print(\"SDK version:\", azureml.core.VERSION)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from azureml.core.workspace import Workspace\n", - "\n", - "ws = Workspace.from_config()\n", - "print('Workspace name: ' + ws.name, \n", - " 'Azure region: ' + ws.location, \n", - " 'Subscription id: ' + ws.subscription_id, \n", - " 'Resource group: ' + ws.resource_group, sep = '\\n')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import getpass\n", - "import os\n", - "from azureml.core.experiment import Experiment\n", - "\n", - "username = getpass.getuser().replace('-','')\n", - "\n", - "# choose a name for the run history container in the workspace\n", - "experiment = Experiment(ws, username + '-horovod')\n", - "\n", - "# project folder name\n", - "project_folder = './samples/distributed-tensorflow-horovod'\n", - "os.makedirs(project_folder, exist_ok = True)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This recipe is using a MLC-managed Batch AI cluster. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from azureml.core.compute import BatchAiCompute\n", - "from azureml.core.compute import ComputeTarget\n", - "\n", - "batchai_cluster_name='gpucluster'\n", - "\n", - "\n", - "try:\n", - " # Check for existing cluster\n", - " compute_target = ComputeTarget(ws,batchai_cluster_name)\n", - " print('Found existing compute target')\n", - "except:\n", - " # Else, create new one\n", - " print('Creating a new compute target...')\n", - " provisioning_config = BatchAiCompute.provisioning_configuration(vm_size = \"STANDARD_NC6\", # NC6 is GPU-enabled\n", - " #vm_priority = 'lowpriority', # optional\n", - " autoscale_enabled = True,\n", - " cluster_min_nodes = 0, \n", - " cluster_max_nodes = 4)\n", - " compute_target = ComputeTarget.create(ws, batchai_cluster_name, provisioning_config)\n", - " # can poll for a minimum number of nodes and for a specific timeout. \n", - " # if no min node count is provided it will use the scale settings for the cluster\n", - " compute_target.wait_for_completion(show_output=True, min_node_count=None, timeout_in_minutes=20)\n", - "\n", - " # For a more detailed view of current BatchAI cluster status, use the 'status' property \n", - "print(compute_target.status.serialize())" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%%writefile {project_folder}/word2vec.py\n", - "\n", - "# Copyright 2015 The TensorFlow Authors. All Rights Reserved.\n", - "# Modifications copyright (C) 2017 Uber Technologies, Inc.\n", - "#\n", - "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", - "# you may not use this file except in compliance with the License.\n", - "# You may obtain a copy of the License at\n", - "#\n", - "# http://www.apache.org/licenses/LICENSE-2.0\n", - "#\n", - "# Unless required by applicable law or agreed to in writing, software\n", - "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", - "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", - "# See the License for the specific language governing permissions and\n", - "# limitations under the License.\n", - "# ==============================================================================\n", - "\"\"\"Basic word2vec example.\"\"\"\n", - "\n", - "from __future__ import absolute_import\n", - "from __future__ import division\n", - "from __future__ import print_function\n", - "\n", - "import collections\n", - "import math\n", - "import os\n", - "import random\n", - "import zipfile\n", - "import argparse\n", - "\n", - "import numpy as np\n", - "from six.moves import urllib\n", - "from six.moves import xrange # pylint: disable=redefined-builtin\n", - "import tensorflow as tf\n", - "import horovod.tensorflow as hvd\n", - "from azureml.core.run import Run\n", - "\n", - "# Horovod: initialize Horovod.\n", - "hvd.init()\n", - "\n", - "parser = argparse.ArgumentParser()\n", - "parser.add_argument('--data_dir', type=str, help='input directory')\n", - "\n", - "args = parser.parse_args()\n", - "\n", - "data_dir = args.data_dir\n", - "print(\"the input data_dir is %s\" % data_dir)\n", - "\n", - "# Step 1: Download the data.\n", - "url = 'http://mattmahoney.net/dc/text8.zip'\n", - "\n", - "\n", - "def maybe_download(filename, expected_bytes):\n", - " \"\"\"Download a file if not present, and make sure it's the right size.\"\"\"\n", - " if not filename:\n", - " filename = \"text8.zip\"\n", - " if not os.path.exists(filename):\n", - " print(\"Downloading the data from http://mattmahoney.net/dc/text8.zip\")\n", - " filename, _ = urllib.request.urlretrieve(url, filename)\n", - " else:\n", - " print(\"Use the data from the input data_dir %s\" % data_dir)\n", - " statinfo = os.stat(filename)\n", - " if statinfo.st_size == expected_bytes:\n", - " print('Found and verified', filename)\n", - " else:\n", - " print(statinfo.st_size)\n", - " raise Exception(\n", - " 'Failed to verify ' + url + '. Can you get to it with a browser?')\n", - " return filename\n", - "\n", - "filename = maybe_download(data_dir, 31344016)\n", - "\n", - "\n", - "# Read the data into a list of strings.\n", - "def read_data(filename):\n", - " \"\"\"Extract the first file enclosed in a zip file as a list of words.\"\"\"\n", - " with zipfile.ZipFile(filename) as f:\n", - " data = tf.compat.as_str(f.read(f.namelist()[0])).split()\n", - " return data\n", - "\n", - "vocabulary = read_data(filename)\n", - "print('Data size', len(vocabulary))\n", - "\n", - "# Step 2: Build the dictionary and replace rare words with UNK token.\n", - "vocabulary_size = 50000\n", - "\n", - "\n", - "def build_dataset(words, n_words):\n", - " \"\"\"Process raw inputs into a dataset.\"\"\"\n", - " count = [['UNK', -1]]\n", - " count.extend(collections.Counter(words).most_common(n_words - 1))\n", - " dictionary = dict()\n", - " for word, _ in count:\n", - " dictionary[word] = len(dictionary)\n", - " data = list()\n", - " unk_count = 0\n", - " for word in words:\n", - " if word in dictionary:\n", - " index = dictionary[word]\n", - " else:\n", - " index = 0 # dictionary['UNK']\n", - " unk_count += 1\n", - " data.append(index)\n", - " count[0][1] = unk_count\n", - " reversed_dictionary = dict(zip(dictionary.values(), dictionary.keys()))\n", - " return data, count, dictionary, reversed_dictionary\n", - "\n", - "data, count, dictionary, reverse_dictionary = build_dataset(vocabulary,\n", - " vocabulary_size)\n", - "del vocabulary # Hint to reduce memory.\n", - "print('Most common words (+UNK)', count[:5])\n", - "print('Sample data', data[:10], [reverse_dictionary[i] for i in data[:10]])\n", - "\n", - "\n", - "# Step 3: Function to generate a training batch for the skip-gram model.\n", - "def generate_batch(batch_size, num_skips, skip_window):\n", - " assert num_skips <= 2 * skip_window\n", - " # Adjust batch_size to match num_skips\n", - " batch_size = batch_size // num_skips * num_skips\n", - " span = 2 * skip_window + 1 # [ skip_window target skip_window ]\n", - " # Backtrack a little bit to avoid skipping words in the end of a batch\n", - " data_index = random.randint(0, len(data) - span - 1)\n", - " batch = np.ndarray(shape=(batch_size), dtype=np.int32)\n", - " labels = np.ndarray(shape=(batch_size, 1), dtype=np.int32)\n", - " buffer = collections.deque(maxlen=span)\n", - " for _ in range(span):\n", - " buffer.append(data[data_index])\n", - " data_index = (data_index + 1) % len(data)\n", - " for i in range(batch_size // num_skips):\n", - " target = skip_window # target label at the center of the buffer\n", - " targets_to_avoid = [skip_window]\n", - " for j in range(num_skips):\n", - " while target in targets_to_avoid:\n", - " target = random.randint(0, span - 1)\n", - " targets_to_avoid.append(target)\n", - " batch[i * num_skips + j] = buffer[skip_window]\n", - " labels[i * num_skips + j, 0] = buffer[target]\n", - " buffer.append(data[data_index])\n", - " data_index = (data_index + 1) % len(data)\n", - " return batch, labels\n", - "\n", - "batch, labels = generate_batch(batch_size=8, num_skips=2, skip_window=1)\n", - "for i in range(8):\n", - " print(batch[i], reverse_dictionary[batch[i]],\n", - " '->', labels[i, 0], reverse_dictionary[labels[i, 0]])\n", - "\n", - "# Step 4: Build and train a skip-gram model.\n", - "\n", - "max_batch_size = 128\n", - "embedding_size = 128 # Dimension of the embedding vector.\n", - "skip_window = 1 # How many words to consider left and right.\n", - "num_skips = 2 # How many times to reuse an input to generate a label.\n", - "\n", - "# We pick a random validation set to sample nearest neighbors. Here we limit the\n", - "# validation samples to the words that have a low numeric ID, which by\n", - "# construction are also the most frequent.\n", - "valid_size = 16 # Random set of words to evaluate similarity on.\n", - "valid_window = 100 # Only pick dev samples in the head of the distribution.\n", - "valid_examples = np.random.choice(valid_window, valid_size, replace=False)\n", - "num_sampled = 64 # Number of negative examples to sample.\n", - "\n", - "graph = tf.Graph()\n", - "\n", - "with graph.as_default():\n", - "\n", - " # Input data.\n", - " train_inputs = tf.placeholder(tf.int32, shape=[None])\n", - " train_labels = tf.placeholder(tf.int32, shape=[None, 1])\n", - " valid_dataset = tf.constant(valid_examples, dtype=tf.int32)\n", - "\n", - " # Look up embeddings for inputs.\n", - " embeddings = tf.Variable(\n", - " tf.random_uniform([vocabulary_size, embedding_size], -1.0, 1.0))\n", - " embed = tf.nn.embedding_lookup(embeddings, train_inputs)\n", - "\n", - " # Construct the variables for the NCE loss\n", - " nce_weights = tf.Variable(\n", - " tf.truncated_normal([vocabulary_size, embedding_size],\n", - " stddev=1.0 / math.sqrt(embedding_size)))\n", - " nce_biases = tf.Variable(tf.zeros([vocabulary_size]))\n", - "\n", - " # Compute the average NCE loss for the batch.\n", - " # tf.nce_loss automatically draws a new sample of the negative labels each\n", - " # time we evaluate the loss.\n", - " loss = tf.reduce_mean(\n", - " tf.nn.nce_loss(weights=nce_weights,\n", - " biases=nce_biases,\n", - " labels=train_labels,\n", - " inputs=embed,\n", - " num_sampled=num_sampled,\n", - " num_classes=vocabulary_size))\n", - "\n", - " # Horovod: adjust learning rate based on number of GPUs.\n", - " optimizer = tf.train.GradientDescentOptimizer(1.0 * hvd.size())\n", - "\n", - " # Horovod: add Horovod Distributed Optimizer.\n", - " optimizer = hvd.DistributedOptimizer(optimizer)\n", - "\n", - " train_op = optimizer.minimize(loss)\n", - "\n", - " # Compute the cosine similarity between minibatch examples and all embeddings.\n", - " norm = tf.sqrt(tf.reduce_sum(tf.square(embeddings), 1, keep_dims=True))\n", - " normalized_embeddings = embeddings / norm\n", - " valid_embeddings = tf.nn.embedding_lookup(\n", - " normalized_embeddings, valid_dataset)\n", - " similarity = tf.matmul(\n", - " valid_embeddings, normalized_embeddings, transpose_b=True)\n", - "\n", - " # Add variable initializer.\n", - " init = tf.global_variables_initializer()\n", - "\n", - " # Horovod: broadcast initial variable states from rank 0 to all other processes.\n", - " # This is necessary to ensure consistent initialization of all workers when\n", - " # training is started with random weights or restored from a checkpoint.\n", - " bcast = hvd.broadcast_global_variables(0)\n", - "\n", - "# Step 5: Begin training.\n", - "\n", - "# Horovod: adjust number of steps based on number of GPUs.\n", - "num_steps = 4000 // hvd.size() + 1\n", - "\n", - "# Horovod: pin GPU to be used to process local rank (one GPU per process)\n", - "config = tf.ConfigProto()\n", - "config.gpu_options.allow_growth = True\n", - "config.gpu_options.visible_device_list = str(hvd.local_rank())\n", - "\n", - "with tf.Session(graph=graph, config=config) as session:\n", - " # We must initialize all variables before we use them.\n", - " init.run()\n", - " bcast.run()\n", - " print('Initialized')\n", - " run = Run.get_submitted_run()\n", - " average_loss = 0\n", - " for step in xrange(num_steps):\n", - " # simulate various sentence length by randomization\n", - " batch_size = random.randint(max_batch_size // 2, max_batch_size)\n", - " batch_inputs, batch_labels = generate_batch(\n", - " batch_size, num_skips, skip_window)\n", - " feed_dict = {train_inputs: batch_inputs, train_labels: batch_labels}\n", - "\n", - " # We perform one update step by evaluating the optimizer op (including it\n", - " # in the list of returned values for session.run()\n", - " _, loss_val = session.run([train_op, loss], feed_dict=feed_dict)\n", - " average_loss += loss_val\n", - "\n", - " if step % 2000 == 0:\n", - " if step > 0:\n", - " average_loss /= 2000\n", - " # The average loss is an estimate of the loss over the last 2000 batches.\n", - " print('Average loss at step ', step, ': ', average_loss)\n", - " run.log(\"Loss\", average_loss)\n", - " average_loss = 0\n", - " final_embeddings = normalized_embeddings.eval()\n", - "\n", - " # Evaluate similarity in the end on worker 0.\n", - " if hvd.rank() == 0:\n", - " sim = similarity.eval()\n", - " for i in xrange(valid_size):\n", - " valid_word = reverse_dictionary[valid_examples[i]]\n", - " top_k = 8 # number of nearest neighbors\n", - " nearest = (-sim[i, :]).argsort()[1:top_k + 1]\n", - " log_str = 'Nearest to %s:' % valid_word\n", - " for k in xrange(top_k):\n", - " close_word = reverse_dictionary[nearest[k]]\n", - " log_str = '%s %s,' % (log_str, close_word)\n", - " print(log_str)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Upload http://mattmahoney.net/dc/text8.zip to the azure blob storage." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "ds = ws.get_default_datastore()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import os\n", - "import urllib\n", - "\n", - "os.makedirs('./data', exist_ok = True)\n", - "\n", - "urllib.request.urlretrieve('http://mattmahoney.net/dc/text8.zip', filename = './data/text8.zip')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "ds.upload(src_dir = 'data', target_path = 'data', overwrite=True, show_progress = True)\n", - "\n", - "path_on_datastore = \"/data/text8.zip\"\n", - "ds_data = ds.path(path_on_datastore)\n", - "print(ds_data)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from azureml.train.dnn import *\n", - "script_params={\n", - " \"--data_dir\": ds_data\n", - "}\n", - "tf_estimator = TensorFlow(source_directory=project_folder,\n", - " compute_target=compute_target,\n", - " entry_script='word2vec.py',\n", - " script_params=script_params,\n", - " node_count=2,\n", - " process_count_per_node=1,\n", - " distributed_backend=\"mpi\",\n", - " use_gpu=False)\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "run = experiment.submit(tf_estimator)\n", - "print(run)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from azureml.train.widgets import RunDetails\n", - "RunDetails(run).show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "run.wait_for_completion(show_output=True)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python [default]", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.6" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/training/51.distributed-tensorflow-with-parameter-server/51.distributed-tensorflow-with-parameter-server.ipynb b/training/51.distributed-tensorflow-with-parameter-server/51.distributed-tensorflow-with-parameter-server.ipynb deleted file mode 100644 index 55decdf03..000000000 --- a/training/51.distributed-tensorflow-with-parameter-server/51.distributed-tensorflow-with-parameter-server.ipynb +++ /dev/null @@ -1,473 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Copyright (c) Microsoft Corporation. All rights reserved.\n", - "\n", - "Licensed under the MIT License." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# 51. Distributed TensorFlow using Parameter Server\n", - "In this tutorial we demonstrate how to use the Azure ML Training SDK to train Tensorflow model in a distributed manner using Parameter Server.\n", - "\n", - "# Prerequisites\n", - "\n", - "Make sure you go through the [00. Installation and Configuration](00.configuration.ipynb) Notebook first if you haven't." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Check core SDK version number\n", - "import azureml.core\n", - "\n", - "print(\"SDK version:\", azureml.core.VERSION)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from azureml.core.workspace import Workspace\n", - "\n", - "ws = Workspace.from_config()\n", - "print('Workspace name: ' + ws.name, \n", - " 'Azure region: ' + ws.location, \n", - " 'Subscription id: ' + ws.subscription_id, \n", - " 'Resource group: ' + ws.resource_group, sep = '\\n')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import getpass\n", - "import os\n", - "from azureml.core.experiment import Experiment\n", - "\n", - "username = getpass.getuser().replace('-','')\n", - "\n", - "# choose a name for the run history container in the workspace\n", - "run_history_name = username + '-tf_ps'\n", - "\n", - "experiment = Experiment(ws, run_history_name)\n", - "\n", - "# project folder name\n", - "project_folder = './' + run_history_name\n", - "\n", - "print(project_folder)\n", - "os.makedirs(project_folder, exist_ok = True)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This recipe is using a MLC-managed Batch AI cluster. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from azureml.core.compute import BatchAiCompute\n", - "from azureml.core.compute import ComputeTarget\n", - "\n", - "batchai_cluster_name='gpucluster'\n", - "\n", - "\n", - "try:\n", - " # Check for existing cluster\n", - " compute_target = ComputeTarget(ws,batchai_cluster_name)\n", - " print('Found existing compute target')\n", - "except:\n", - " # Else, create new one\n", - " print('Creating a new compute target...')\n", - " provisioning_config = BatchAiCompute.provisioning_configuration(vm_size = \"STANDARD_NC6\", # NC6 is GPU-enabled\n", - " #vm_priority = 'lowpriority', # optional\n", - " autoscale_enabled = True,\n", - " cluster_min_nodes = 0, \n", - " cluster_max_nodes = 4)\n", - " compute_target = ComputeTarget.create(ws, batchai_cluster_name, provisioning_config)\n", - " # can poll for a minimum number of nodes and for a specific timeout. \n", - " # if no min node count is provided it will use the scale settings for the cluster\n", - " compute_target.wait_for_completion(show_output=True, min_node_count=None, timeout_in_minutes=20)\n", - "\n", - " # For a more detailed view of current BatchAI cluster status, use the 'status' property \n", - "print(compute_target.status.serialize())" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%%writefile {project_folder}/mnist_replica.py\n", - "\n", - "# Copyright 2016 The TensorFlow Authors. All Rights Reserved.\n", - "#\n", - "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", - "# you may not use this file except in compliance with the License.\n", - "# You may obtain a copy of the License at\n", - "#\n", - "# http://www.apache.org/licenses/LICENSE-2.0\n", - "#\n", - "# Unless required by applicable law or agreed to in writing, software\n", - "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", - "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", - "# See the License for the specific language governing permissions and\n", - "# limitations under the License.\n", - "# ==============================================================================\n", - "\"\"\"Distributed MNIST training and validation, with model replicas.\n", - "A simple softmax model with one hidden layer is defined. The parameters\n", - "(weights and biases) are located on one parameter server (ps), while the ops\n", - "are executed on two worker nodes by default. The TF sessions also run on the\n", - "worker node.\n", - "Multiple invocations of this script can be done in parallel, with different\n", - "values for --task_index. There should be exactly one invocation with\n", - "--task_index, which will create a master session that carries out variable\n", - "initialization. The other, non-master, sessions will wait for the master\n", - "session to finish the initialization before proceeding to the training stage.\n", - "The coordination between the multiple worker invocations occurs due to\n", - "the definition of the parameters on the same ps devices. The parameter updates\n", - "from one worker is visible to all other workers. As such, the workers can\n", - "perform forward computation and gradient calculation in parallel, which\n", - "should lead to increased training speed for the simple model.\n", - "\"\"\"\n", - "\n", - "from __future__ import absolute_import\n", - "from __future__ import division\n", - "from __future__ import print_function\n", - "\n", - "import os\n", - "import math\n", - "import sys\n", - "import tempfile\n", - "import time\n", - "import json\n", - "\n", - "import tensorflow as tf\n", - "from tensorflow.examples.tutorials.mnist import input_data\n", - "from azureml.core.run import Run\n", - "\n", - "flags = tf.app.flags\n", - "flags.DEFINE_string(\"data_dir\", \"/tmp/mnist-data\",\n", - " \"Directory for storing mnist data\")\n", - "flags.DEFINE_boolean(\"download_only\", False,\n", - " \"Only perform downloading of data; Do not proceed to \"\n", - " \"session preparation, model definition or training\")\n", - "flags.DEFINE_integer(\"num_gpus\", 0, \"Total number of gpus for each machine.\"\n", - " \"If you don't use GPU, please set it to '0'\")\n", - "flags.DEFINE_integer(\"replicas_to_aggregate\", None,\n", - " \"Number of replicas to aggregate before parameter update \"\n", - " \"is applied (For sync_replicas mode only; default: \"\n", - " \"num_workers)\")\n", - "flags.DEFINE_integer(\"hidden_units\", 100,\n", - " \"Number of units in the hidden layer of the NN\")\n", - "flags.DEFINE_integer(\"train_steps\", 200,\n", - " \"Number of (global) training steps to perform\")\n", - "flags.DEFINE_integer(\"batch_size\", 100, \"Training batch size\")\n", - "flags.DEFINE_float(\"learning_rate\", 0.01, \"Learning rate\")\n", - "flags.DEFINE_boolean(\n", - " \"sync_replicas\", False,\n", - " \"Use the sync_replicas (synchronized replicas) mode, \"\n", - " \"wherein the parameter updates from workers are aggregated \"\n", - " \"before applied to avoid stale gradients\")\n", - "flags.DEFINE_boolean(\n", - " \"existing_servers\", False, \"Whether servers already exists. If True, \"\n", - " \"will use the worker hosts via their GRPC URLs (one client process \"\n", - " \"per worker host). Otherwise, will create an in-process TensorFlow \"\n", - " \"server.\")\n", - "\n", - "FLAGS = flags.FLAGS\n", - "\n", - "IMAGE_PIXELS = 28\n", - "\n", - "\n", - "def main(unused_argv):\n", - " data_root = os.path.join(\"outputs\", \"MNIST\")\n", - " mnist = None\n", - " tf_config = os.environ.get(\"TF_CONFIG\")\n", - " if not tf_config or tf_config == \"\":\n", - " raise ValueError(\"TF_CONFIG not found.\")\n", - " tf_config_json = json.loads(tf_config)\n", - " cluster = tf_config_json.get('cluster')\n", - " job_name = tf_config_json.get('task', {}).get('type')\n", - " task_index = tf_config_json.get('task', {}).get('index')\n", - " job_name = \"worker\" if job_name == \"master\" else job_name\n", - " sentinel_path = os.path.join(data_root, \"complete.txt\") \n", - " if job_name==\"worker\" and task_index==0:\n", - " mnist = input_data.read_data_sets(data_root, one_hot=True)\n", - " path = os.path.join(data_root, \"complete.txt\") \n", - " with open(sentinel_path, 'w+') as f:\n", - " f.write(\"download complete\")\n", - " else:\n", - " while not os.path.exists(sentinel_path):\n", - " time.sleep(0.01)\n", - " mnist = input_data.read_data_sets(data_root, one_hot=True)\n", - " \n", - " if FLAGS.download_only:\n", - " sys.exit(0)\n", - "\n", - " print(\"job name = %s\" % job_name)\n", - " print(\"task index = %d\" % task_index)\n", - " print(\"number of GPUs = %d\" % FLAGS.num_gpus)\n", - "\n", - " #Construct the cluster and start the server\n", - " cluster_spec = tf.train.ClusterSpec(cluster)\n", - " \n", - " # Get the number of workers.\n", - " num_workers = len(cluster_spec.task_indices(\"worker\"))\n", - "\n", - " if not FLAGS.existing_servers:\n", - " # Not using existing servers. Create an in-process server.\n", - " server = tf.train.Server(\n", - " cluster_spec, job_name=job_name, task_index=task_index)\n", - " if job_name == \"ps\":\n", - " server.join()\n", - "\n", - " is_chief = (task_index == 0)\n", - " if FLAGS.num_gpus > 0:\n", - " # Avoid gpu allocation conflict: now allocate task_num -> #gpu\n", - " # for each worker in the corresponding machine\n", - " gpu = (task_index % FLAGS.num_gpus)\n", - " worker_device = \"/job:worker/task:%d/gpu:%d\" % (task_index, gpu)\n", - " elif FLAGS.num_gpus == 0:\n", - " # Just allocate the CPU to worker server\n", - " cpu = 0\n", - " worker_device = \"/job:worker/task:%d/cpu:%d\" % (task_index, cpu)\n", - " # The device setter will automatically place Variables ops on separate\n", - " # parameter servers (ps). The non-Variable ops will be placed on the workers.\n", - " # The ps use CPU and workers use corresponding GPU\n", - " with tf.device(\n", - " tf.train.replica_device_setter(\n", - " worker_device=worker_device,\n", - " ps_device=\"/job:ps/cpu:0\",\n", - " cluster=cluster)):\n", - " global_step = tf.Variable(0, name=\"global_step\", trainable=False)\n", - "\n", - " # Variables of the hidden layer\n", - " hid_w = tf.Variable(\n", - " tf.truncated_normal(\n", - " [IMAGE_PIXELS * IMAGE_PIXELS, FLAGS.hidden_units],\n", - " stddev=1.0 / IMAGE_PIXELS),\n", - " name=\"hid_w\")\n", - " hid_b = tf.Variable(tf.zeros([FLAGS.hidden_units]), name=\"hid_b\")\n", - "\n", - " # Variables of the softmax layer\n", - " sm_w = tf.Variable(\n", - " tf.truncated_normal(\n", - " [FLAGS.hidden_units, 10],\n", - " stddev=1.0 / math.sqrt(FLAGS.hidden_units)),\n", - " name=\"sm_w\")\n", - " sm_b = tf.Variable(tf.zeros([10]), name=\"sm_b\")\n", - "\n", - " # Ops: located on the worker specified with task_index\n", - " x = tf.placeholder(tf.float32, [None, IMAGE_PIXELS * IMAGE_PIXELS])\n", - " y_ = tf.placeholder(tf.float32, [None, 10])\n", - "\n", - " hid_lin = tf.nn.xw_plus_b(x, hid_w, hid_b)\n", - " hid = tf.nn.relu(hid_lin)\n", - "\n", - " y = tf.nn.softmax(tf.nn.xw_plus_b(hid, sm_w, sm_b))\n", - " cross_entropy = -tf.reduce_sum(y_ * tf.log(tf.clip_by_value(y, 1e-10, 1.0)))\n", - "\n", - " opt = tf.train.AdamOptimizer(FLAGS.learning_rate)\n", - "\n", - " if FLAGS.sync_replicas:\n", - " if FLAGS.replicas_to_aggregate is None:\n", - " replicas_to_aggregate = num_workers\n", - " else:\n", - " replicas_to_aggregate = FLAGS.replicas_to_aggregate\n", - "\n", - " opt = tf.train.SyncReplicasOptimizer(\n", - " opt,\n", - " replicas_to_aggregate=replicas_to_aggregate,\n", - " total_num_replicas=num_workers,\n", - " name=\"mnist_sync_replicas\")\n", - "\n", - " train_step = opt.minimize(cross_entropy, global_step=global_step)\n", - "\n", - " if FLAGS.sync_replicas:\n", - " local_init_op = opt.local_step_init_op\n", - " if is_chief:\n", - " local_init_op = opt.chief_init_op\n", - "\n", - " ready_for_local_init_op = opt.ready_for_local_init_op\n", - "\n", - " # Initial token and chief queue runners required by the sync_replicas mode\n", - " chief_queue_runner = opt.get_chief_queue_runner()\n", - " sync_init_op = opt.get_init_tokens_op()\n", - "\n", - " init_op = tf.global_variables_initializer()\n", - " train_dir = tempfile.mkdtemp()\n", - "\n", - " if FLAGS.sync_replicas:\n", - " sv = tf.train.Supervisor(\n", - " is_chief=is_chief,\n", - " logdir=train_dir,\n", - " init_op=init_op,\n", - " local_init_op=local_init_op,\n", - " ready_for_local_init_op=ready_for_local_init_op,\n", - " recovery_wait_secs=1,\n", - " global_step=global_step)\n", - " else:\n", - " sv = tf.train.Supervisor(\n", - " is_chief=is_chief,\n", - " logdir=train_dir,\n", - " init_op=init_op,\n", - " recovery_wait_secs=1,\n", - " global_step=global_step)\n", - "\n", - " sess_config = tf.ConfigProto(\n", - " allow_soft_placement=True,\n", - " log_device_placement=False,\n", - " device_filters=[\"/job:ps\",\n", - " \"/job:worker/task:%d\" % task_index])\n", - "\n", - " # The chief worker (task_index==0) session will prepare the session,\n", - " # while the remaining workers will wait for the preparation to complete.\n", - " if is_chief:\n", - " print(\"Worker %d: Initializing session...\" % task_index)\n", - " else:\n", - " print(\"Worker %d: Waiting for session to be initialized...\" %\n", - " task_index)\n", - "\n", - " if FLAGS.existing_servers:\n", - " server_grpc_url = \"grpc://\" + worker_spec[task_index]\n", - " print(\"Using existing server at: %s\" % server_grpc_url)\n", - "\n", - " sess = sv.prepare_or_wait_for_session(server_grpc_url, config=sess_config)\n", - " else:\n", - " sess = sv.prepare_or_wait_for_session(server.target, config=sess_config)\n", - "\n", - " print(\"Worker %d: Session initialization complete.\" % task_index)\n", - "\n", - " if FLAGS.sync_replicas and is_chief:\n", - " # Chief worker will start the chief queue runner and call the init op.\n", - " sess.run(sync_init_op)\n", - " sv.start_queue_runners(sess, [chief_queue_runner])\n", - "\n", - " # Perform training\n", - " time_begin = time.time()\n", - " print(\"Training begins @ %f\" % time_begin)\n", - "\n", - " local_step = 0\n", - " while True:\n", - " # Training feed\n", - " batch_xs, batch_ys = mnist.train.next_batch(FLAGS.batch_size)\n", - " train_feed = {x: batch_xs, y_: batch_ys}\n", - "\n", - " _, step = sess.run([train_step, global_step], feed_dict=train_feed)\n", - " local_step += 1\n", - "\n", - " now = time.time()\n", - " print(\"%f: Worker %d: training step %d done (global step: %d)\" %\n", - " (now, task_index, local_step, step))\n", - "\n", - " if step >= FLAGS.train_steps:\n", - " break\n", - "\n", - " time_end = time.time()\n", - " print(\"Training ends @ %f\" % time_end)\n", - " training_time = time_end - time_begin\n", - " print(\"Training elapsed time: %f s\" % training_time)\n", - "\n", - " # Validation feed\n", - " val_feed = {x: mnist.validation.images, y_: mnist.validation.labels}\n", - " val_xent = sess.run(cross_entropy, feed_dict=val_feed)\n", - " print(\"After %d training step(s), validation cross entropy = %g\" %\n", - " (FLAGS.train_steps, val_xent))\n", - " if job_name==\"worker\" and task_index==0:\n", - " run = Run.get_submitted_run()\n", - " run.log(\"CrossEntropy\", val_xent)\n", - "\n", - "if __name__ == \"__main__\":\n", - " tf.app.run()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from azureml.train.dnn import *\n", - "tf_estimator = TensorFlow(source_directory=project_folder,\n", - " compute_target=compute_target,\n", - " entry_script='mnist_replica.py',\n", - " node_count=2,\n", - " worker_count=2,\n", - " parameter_server_count=1, \n", - " distributed_backend=\"ps\",\n", - " use_gpu=False)\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "run = experiment.submit(tf_estimator)\n", - "print(run)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from azureml.train.widgets import RunDetails\n", - "RunDetails(run).show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "run.wait_for_completion(show_output=True)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.5" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/training/52.distributed-cntk/52.distributed-cntk.ipynb b/training/52.distributed-cntk/52.distributed-cntk.ipynb deleted file mode 100644 index 38c566875..000000000 --- a/training/52.distributed-cntk/52.distributed-cntk.ipynb +++ /dev/null @@ -1,509 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Copyright (c) Microsoft Corporation. All rights reserved.\n", - "\n", - "Licensed under the MIT License." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# 52. Distributed CNTK\n", - "In this tutorial we demonstrate how to use the Azure ML Training SDK to train CNTK model in a distributed manner.\n", - "\n", - "# Prerequisites\n", - "\n", - "Make sure you go through the [00. Installation and Configuration](00.configuration.ipynb) Notebook first if you haven't." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Check core SDK version number\n", - "import azureml.core\n", - "\n", - "print(\"SDK version:\", azureml.core.VERSION)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from azureml.core.workspace import Workspace\n", - "\n", - "ws = Workspace.from_config()\n", - "print('Workspace name: ' + ws.name, \n", - " 'Azure region: ' + ws.location, \n", - " 'Subscription id: ' + ws.subscription_id, \n", - " 'Resource group: ' + ws.resource_group, sep = '\\n')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import getpass\n", - "import os\n", - "from azureml.core.experiment import Experiment\n", - "\n", - "username = getpass.getuser().replace('-','')\n", - "\n", - "# choose a name for the run history container in the workspace\n", - "run_history_name = username + '-cntk-distrib'\n", - "\n", - "experiment = Experiment(ws, run_history_name)\n", - "\n", - "# project folder name\n", - "project_folder = './' + run_history_name\n", - "\n", - "print(project_folder)\n", - "os.makedirs(project_folder, exist_ok = True)\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This recipe is using a MLC-managed Batch AI cluster. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from azureml.core.compute import BatchAiCompute\n", - "from azureml.core.compute import ComputeTarget\n", - "\n", - "batchai_cluster_name='gpucluster'\n", - "\n", - "\n", - "try:\n", - " # Check for existing cluster\n", - " compute_target = ComputeTarget(ws,batchai_cluster_name)\n", - " print('Found existing compute target')\n", - "except:\n", - " # Else, create new one\n", - " print('Creating a new compute target...')\n", - " provisioning_config = BatchAiCompute.provisioning_configuration(vm_size = \"STANDARD_NC6\", # NC6 is GPU-enabled\n", - " #vm_priority = 'lowpriority', # optional\n", - " autoscale_enabled = True,\n", - " cluster_min_nodes = 0, \n", - " cluster_max_nodes = 4)\n", - " compute_target = ComputeTarget.create(ws, batchai_cluster_name, provisioning_config)\n", - " # can poll for a minimum number of nodes and for a specific timeout. \n", - " # if no min node count is provided it will use the scale settings for the cluster\n", - " compute_target.wait_for_completion(show_output=True, min_node_count=None, timeout_in_minutes=20)\n", - "\n", - " # For a more detailed view of current BatchAI cluster status, use the 'status' property \n", - "print(compute_target.status.serialize())" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%%writefile {project_folder}/cntk_mnist.py\n", - "\n", - "# This code is adapted from CNTK MNIST tutorials: \n", - "# 1. https://github.com/Microsoft/CNTK/blob/v2.0/Tutorials/CNTK_103A_MNIST_DataLoader.ipynb\n", - "# 2. https://github.com/Microsoft/CNTK/blob/v2.0/Tutorials/CNTK_103C_MNIST_MultiLayerPerceptron.ipynb\n", - "\n", - "# Import the relevant modules to be used later\n", - "from __future__ import print_function\n", - "import gzip\n", - "import numpy as np\n", - "import os\n", - "import shutil\n", - "import struct\n", - "import sys\n", - "import time\n", - "import pandas \n", - "\n", - "import cntk as C\n", - "from azureml.core.run import Run\n", - "import argparse\n", - "\n", - "run = Run.get_submitted_run()\n", - "\n", - "parser=argparse.ArgumentParser()\n", - "\n", - "parser.add_argument('--learning_rate', type=float, default=0.001, help='learning rate')\n", - "parser.add_argument('--num_hidden_layers', type=int, default=2, help='number of hidden layers')\n", - "parser.add_argument('--minibatch_size', type=int, default=64, help='minibatchsize')\n", - "\n", - "args=parser.parse_args() \n", - "\n", - "# Functions to load MNIST images and unpack into train and test set.\n", - "# - loadData reads image data and formats into a 28x28 long array\n", - "# - loadLabels reads the corresponding labels data, 1 for each image\n", - "# - load packs the downloaded image and labels data into a combined format to be read later by \n", - "# CNTK text reader \n", - "def loadData(src, cimg):\n", - " print ('Downloading ' + src)\n", - " gzfname, h = urlretrieve(src, './delete.me')\n", - " print ('Done.')\n", - " try:\n", - " with gzip.open(gzfname) as gz:\n", - " n = struct.unpack('I', gz.read(4))\n", - " # Read magic number.\n", - " if n[0] != 0x3080000:\n", - " raise Exception('Invalid file: unexpected magic number.')\n", - " # Read number of entries.\n", - " n = struct.unpack('>I', gz.read(4))[0]\n", - " if n != cimg:\n", - " raise Exception('Invalid file: expected {0} entries.'.format(cimg))\n", - " crow = struct.unpack('>I', gz.read(4))[0]\n", - " ccol = struct.unpack('>I', gz.read(4))[0]\n", - " if crow != 28 or ccol != 28:\n", - " raise Exception('Invalid file: expected 28 rows/cols per image.')\n", - " # Read data.\n", - " res = np.fromstring(gz.read(cimg * crow * ccol), dtype = np.uint8)\n", - " finally:\n", - " os.remove(gzfname)\n", - " return res.reshape((cimg, crow * ccol))\n", - "\n", - "def loadLabels(src, cimg):\n", - " print ('Downloading ' + src)\n", - " gzfname, h = urlretrieve(src, './delete.me')\n", - " print ('Done.')\n", - " try:\n", - " with gzip.open(gzfname) as gz:\n", - " n = struct.unpack('I', gz.read(4))\n", - " # Read magic number.\n", - " if n[0] != 0x1080000:\n", - " raise Exception('Invalid file: unexpected magic number.')\n", - " # Read number of entries.\n", - " n = struct.unpack('>I', gz.read(4))\n", - " if n[0] != cimg:\n", - " raise Exception('Invalid file: expected {0} rows.'.format(cimg))\n", - " # Read labels.\n", - " res = np.fromstring(gz.read(cimg), dtype = np.uint8)\n", - " finally:\n", - " os.remove(gzfname)\n", - " return res.reshape((cimg, 1))\n", - "\n", - "def try_download(dataSrc, labelsSrc, cimg):\n", - " data = loadData(dataSrc, cimg)\n", - " labels = loadLabels(labelsSrc, cimg)\n", - " return np.hstack((data, labels))\n", - "\n", - "# Save the data files into a format compatible with CNTK text reader\n", - "def savetxt(filename, ndarray):\n", - " dir = os.path.dirname(filename)\n", - "\n", - " if not os.path.exists(dir):\n", - " os.makedirs(dir)\n", - "\n", - " if not os.path.isfile(filename):\n", - " print(\"Saving\", filename )\n", - " with open(filename, 'w') as f:\n", - " labels = list(map(' '.join, np.eye(10, dtype=np.uint).astype(str)))\n", - " for row in ndarray:\n", - " row_str = row.astype(str)\n", - " label_str = labels[row[-1]]\n", - " feature_str = ' '.join(row_str[:-1])\n", - " f.write('|labels {} |features {}\\n'.format(label_str, feature_str))\n", - " else:\n", - " print(\"File already exists\", filename)\n", - "\n", - "# Read a CTF formatted text (as mentioned above) using the CTF deserializer from a file\n", - "def create_reader(path, is_training, input_dim, num_label_classes):\n", - " return C.io.MinibatchSource(C.io.CTFDeserializer(path, C.io.StreamDefs(\n", - " labels = C.io.StreamDef(field='labels', shape=num_label_classes, is_sparse=False),\n", - " features = C.io.StreamDef(field='features', shape=input_dim, is_sparse=False)\n", - " )), randomize = is_training, max_sweeps = C.io.INFINITELY_REPEAT if is_training else 1)\n", - "\n", - "# Defines a utility that prints the training progress\n", - "def print_training_progress(trainer, mb, frequency, verbose=1):\n", - " training_loss = \"NA\"\n", - " eval_error = \"NA\"\n", - "\n", - " if mb%frequency == 0:\n", - " training_loss = trainer.previous_minibatch_loss_average\n", - " eval_error = trainer.previous_minibatch_evaluation_average\n", - " if verbose: \n", - " print (\"Minibatch: {0}, Loss: {1:.4f}, Error: {2:.2f}%\".format(mb, training_loss, eval_error*100))\n", - " \n", - " return mb, training_loss, eval_error\n", - "\n", - "# Create the network architecture\n", - "def create_model(features):\n", - " with C.layers.default_options(init = C.layers.glorot_uniform(), activation = C.ops.relu):\n", - " h = features\n", - " for _ in range(num_hidden_layers):\n", - " h = C.layers.Dense(hidden_layers_dim)(h)\n", - " r = C.layers.Dense(num_output_classes, activation = None)(h)\n", - " return r\n", - "\n", - "\n", - "if __name__ == '__main__':\n", - " run = Run.get_submitted_run()\n", - "\n", - " try: \n", - " from urllib.request import urlretrieve \n", - " except ImportError: \n", - " from urllib import urlretrieve\n", - "\n", - " # Select the right target device when this script is being used:\n", - " if 'TEST_DEVICE' in os.environ:\n", - " if os.environ['TEST_DEVICE'] == 'cpu':\n", - " C.device.try_set_default_device(C.device.cpu())\n", - " else:\n", - " C.device.try_set_default_device(C.device.gpu(0))\n", - "\n", - " # URLs for the train image and labels data\n", - " url_train_image = 'http://yann.lecun.com/exdb/mnist/train-images-idx3-ubyte.gz'\n", - " url_train_labels = 'http://yann.lecun.com/exdb/mnist/train-labels-idx1-ubyte.gz'\n", - " num_train_samples = 60000\n", - "\n", - " print(\"Downloading train data\")\n", - " train = try_download(url_train_image, url_train_labels, num_train_samples)\n", - "\n", - " url_test_image = 'http://yann.lecun.com/exdb/mnist/t10k-images-idx3-ubyte.gz'\n", - " url_test_labels = 'http://yann.lecun.com/exdb/mnist/t10k-labels-idx1-ubyte.gz'\n", - " num_test_samples = 10000\n", - "\n", - " print(\"Downloading test data\")\n", - " test = try_download(url_test_image, url_test_labels, num_test_samples)\n", - "\n", - "\n", - " # Save the train and test files (prefer our default path for the data\n", - " rank = os.environ.get(\"OMPI_COMM_WORLD_RANK\") \n", - " data_dir = os.path.join(\"outputs\", \"MNIST\")\n", - " sentinel_path = os.path.join(data_dir, \"complete.txt\") \n", - " if rank == '0': \n", - " print ('Writing train text file...')\n", - " savetxt(os.path.join(data_dir, \"Train-28x28_cntk_text.txt\"), train)\n", - "\n", - " print ('Writing test text file...')\n", - " savetxt(os.path.join(data_dir, \"Test-28x28_cntk_text.txt\"), test)\n", - " with open(sentinel_path, 'w+') as f:\n", - " f.write(\"download complete\")\n", - "\n", - " print('Done with downloading data.')\n", - " else:\n", - " while not os.path.exists(sentinel_path):\n", - " time.sleep(0.01)\n", - " \n", - "\n", - " # Ensure we always get the same amount of randomness\n", - " np.random.seed(0)\n", - "\n", - " # Define the data dimensions\n", - " input_dim = 784\n", - " num_output_classes = 10\n", - "\n", - " # Ensure the training and test data is generated and available for this tutorial.\n", - " # We search in two locations in the toolkit for the cached MNIST data set.\n", - " data_found = False\n", - " for data_dir in [os.path.join(\"..\", \"Examples\", \"Image\", \"DataSets\", \"MNIST\"),\n", - " os.path.join(\"data_\" + str(rank), \"MNIST\"),\n", - " os.path.join(\"outputs\", \"MNIST\")]:\n", - " train_file = os.path.join(data_dir, \"Train-28x28_cntk_text.txt\")\n", - " test_file = os.path.join(data_dir, \"Test-28x28_cntk_text.txt\")\n", - " if os.path.isfile(train_file) and os.path.isfile(test_file):\n", - " data_found = True\n", - " break\n", - " if not data_found:\n", - " raise ValueError(\"Please generate the data by completing CNTK 103 Part A\")\n", - " print(\"Data directory is {0}\".format(data_dir))\n", - "\n", - " num_hidden_layers = args.num_hidden_layers\n", - " hidden_layers_dim = 400\n", - "\n", - " input = C.input_variable(input_dim)\n", - " label = C.input_variable(num_output_classes)\n", - "\n", - " \n", - " z = create_model(input)\n", - " # Scale the input to 0-1 range by dividing each pixel by 255.\n", - " z = create_model(input/255.0)\n", - "\n", - " loss = C.cross_entropy_with_softmax(z, label)\n", - " label_error = C.classification_error(z, label)\n", - "\n", - "\n", - " # Instantiate the trainer object to drive the model training\n", - " learning_rate = args.learning_rate\n", - " lr_schedule = C.learning_rate_schedule(learning_rate, C.UnitType.minibatch)\n", - " learner = C.sgd(z.parameters, lr_schedule)\n", - " trainer = C.Trainer(z, (loss, label_error), [learner])\n", - "\n", - "\n", - " # Initialize the parameters for the trainer\n", - " minibatch_size = args.minibatch_size\n", - " num_samples_per_sweep = 60000\n", - " num_sweeps_to_train_with = 10\n", - " num_minibatches_to_train = (num_samples_per_sweep * num_sweeps_to_train_with) / minibatch_size\n", - "\n", - " # Create the reader to training data set\n", - " reader_train = create_reader(train_file, True, input_dim, num_output_classes)\n", - "\n", - " # Map the data streams to the input and labels.\n", - " input_map = {\n", - " label : reader_train.streams.labels,\n", - " input : reader_train.streams.features\n", - " } \n", - "\n", - " # Run the trainer on and perform model training\n", - " training_progress_output_freq = 500\n", - " \n", - " errors = []\n", - " losses = []\n", - " for i in range(0, int(num_minibatches_to_train)): \n", - " # Read a mini batch from the training data file\n", - " data = reader_train.next_minibatch(minibatch_size, input_map = input_map)\n", - " \n", - " trainer.train_minibatch(data)\n", - " batchsize, loss, error = print_training_progress(trainer, i, training_progress_output_freq, verbose=1)\n", - " if (error != 'NA') and (loss != 'NA'):\n", - " errors.append(float(error))\n", - " losses.append(float(loss))\n", - " \n", - " # log the losses\n", - " if rank == '0': \n", - " run.log_list(\"Loss\", losses)\n", - " run.log_list(\"Error\",errors)\n", - "\n", - " # Read the training data\n", - " reader_test = create_reader(test_file, False, input_dim, num_output_classes)\n", - "\n", - " test_input_map = {\n", - " label : reader_test.streams.labels,\n", - " input : reader_test.streams.features,\n", - " }\n", - "\n", - " # Test data for trained model\n", - " test_minibatch_size = 512\n", - " num_samples = 10000\n", - " num_minibatches_to_test = num_samples // test_minibatch_size\n", - " test_result = 0.0\n", - "\n", - " \n", - " for i in range(num_minibatches_to_test): \n", - " # We are loading test data in batches specified by test_minibatch_size\n", - " # Each data point in the minibatch is a MNIST digit image of 784 dimensions \n", - " # with one pixel per dimension that we will encode / decode with the \n", - " # trained model.\n", - " data = reader_test.next_minibatch(test_minibatch_size,\n", - " input_map = test_input_map)\n", - "\n", - " eval_error = trainer.test_minibatch(data)\n", - " test_result = test_result + eval_error\n", - " \n", - "\n", - " # Average of evaluation errors of all test minibatches\n", - " print(\"Average test error: {0:.2f}%\".format(test_result*100 / num_minibatches_to_test))\n", - "\n", - " out = C.softmax(z)\n", - "\n", - " # Read the data for evaluation\n", - " reader_eval = create_reader(test_file, False, input_dim, num_output_classes)\n", - "\n", - " eval_minibatch_size = 25\n", - " eval_input_map = {input: reader_eval.streams.features} \n", - "\n", - " data = reader_test.next_minibatch(eval_minibatch_size, input_map = test_input_map)\n", - "\n", - " img_label = data[label].asarray()\n", - " img_data = data[input].asarray()\n", - " predicted_label_prob = [out.eval(img_data[i]) for i in range(len(img_data))]\n", - "\n", - " # Find the index with the maximum value for both predicted as well as the ground truth\n", - " pred = [np.argmax(predicted_label_prob[i]) for i in range(len(predicted_label_prob))]\n", - " gtlabel = [np.argmax(img_label[i]) for i in range(len(img_label))]\n", - "\n", - " print(\"Label :\", gtlabel[:25])\n", - " print(\"Predicted:\", pred)\n", - " \n", - " # save model to outputs folder\n", - " z.save('outputs/cntk.model')\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from azureml.train.estimator import *\n", - "pip_packages=['cntk==2.5.1', 'pandas==0.23.4']\n", - "cntk_estimator = Estimator(source_directory=project_folder,\n", - " compute_target=compute_target,\n", - " entry_script='cntk_mnist.py',\n", - " node_count=2,\n", - " process_count_per_node=1,\n", - " distributed_backend=\"mpi\", \n", - " pip_packages=pip_packages,\n", - " custom_docker_base_image=\"microsoft/mmlspark:0.12\",\n", - " use_gpu=False)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "run = experiment.submit(cntk_estimator)\n", - "print(run)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from azureml.train.widgets import RunDetails\n", - "RunDetails(run).show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "run.wait_for_completion(show_output=True)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.5" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/training/53.distributed-pytorch-with-horovod/53.distributed-pytorch-with-horovod.ipynb b/training/53.distributed-pytorch-with-horovod/53.distributed-pytorch-with-horovod.ipynb deleted file mode 100644 index 46db6dab0..000000000 --- a/training/53.distributed-pytorch-with-horovod/53.distributed-pytorch-with-horovod.ipynb +++ /dev/null @@ -1,376 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Copyright (c) Microsoft Corporation. All rights reserved.\n", - "\n", - "Licensed under the MIT License." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# PyTorch Distributed Demo\n", - "\n", - "In this demo, we will run a sample PyTorch job using Horovod on a multi-node Batch AI cluster." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Prerequisites\n", - "Make sure you go through the [00. Installation and Configuration](00.configuration.ipynb) Notebook first if you haven't." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Check core SDK version number\n", - "import azureml.core\n", - "\n", - "print(\"SDK version:\", azureml.core.VERSION)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Initialize Workspace\n", - "\n", - "Initialize a workspace object from persisted configuration." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from azureml.core.workspace import Workspace\n", - "\n", - "ws = Workspace.from_config()\n", - "print('Workspace name: ' + ws.name, \n", - " 'Azure region: ' + ws.location, \n", - " 'Subscription id: ' + ws.subscription_id, \n", - " 'Resource group: ' + ws.resource_group, sep = '\\n')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Set experiment name and create project\n", - "Choose a name for your run history container in the workspace, and create a folder for the project." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import os\n", - "experiment_name = 'pytorch-dist-hvd'\n", - "\n", - "# project folder\n", - "project_folder = './sample_projects/pytorch-dist-hvd'\n", - "os.makedirs(project_folder, exist_ok = True)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Write demo PyTorch code\n", - "\n", - "We will use a distributed PyTorch implementation of the classic MNIST problem. The following cell writes the main implementation to the project folder." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%%writefile {project_folder}/pytorch_horovod_mnist.py\n", - "\n", - "from __future__ import print_function\n", - "import argparse\n", - "import torch.nn as nn\n", - "import torch.nn.functional as F\n", - "import torch.optim as optim\n", - "from torchvision import datasets, transforms\n", - "from torch.autograd import Variable\n", - "import torch.utils.data.distributed\n", - "import horovod.torch as hvd\n", - "\n", - "# Training settings\n", - "parser = argparse.ArgumentParser(description='PyTorch MNIST Example')\n", - "parser.add_argument('--batch-size', type=int, default=64, metavar='N',\n", - " help='input batch size for training (default: 64)')\n", - "parser.add_argument('--test-batch-size', type=int, default=1000, metavar='N',\n", - " help='input batch size for testing (default: 1000)')\n", - "parser.add_argument('--epochs', type=int, default=10, metavar='N',\n", - " help='number of epochs to train (default: 10)')\n", - "parser.add_argument('--lr', type=float, default=0.01, metavar='LR',\n", - " help='learning rate (default: 0.01)')\n", - "parser.add_argument('--momentum', type=float, default=0.5, metavar='M',\n", - " help='SGD momentum (default: 0.5)')\n", - "parser.add_argument('--no-cuda', action='store_true', default=False,\n", - " help='disables CUDA training')\n", - "parser.add_argument('--seed', type=int, default=42, metavar='S',\n", - " help='random seed (default: 42)')\n", - "parser.add_argument('--log-interval', type=int, default=10, metavar='N',\n", - " help='how many batches to wait before logging training status')\n", - "args = parser.parse_args()\n", - "args.cuda = not args.no_cuda and torch.cuda.is_available()\n", - "\n", - "hvd.init()\n", - "torch.manual_seed(args.seed)\n", - "\n", - "if args.cuda:\n", - " # Horovod: pin GPU to local rank.\n", - " torch.cuda.set_device(hvd.local_rank())\n", - " torch.cuda.manual_seed(args.seed)\n", - "\n", - "\n", - "kwargs = {'num_workers': 1, 'pin_memory': True} if args.cuda else {}\n", - "train_dataset = \\\n", - " datasets.MNIST('data-%d' % hvd.rank(), train=True, download=True,\n", - " transform=transforms.Compose([\n", - " transforms.ToTensor(),\n", - " transforms.Normalize((0.1307,), (0.3081,))\n", - " ]))\n", - "train_sampler = torch.utils.data.distributed.DistributedSampler(\n", - " train_dataset, num_replicas=hvd.size(), rank=hvd.rank())\n", - "train_loader = torch.utils.data.DataLoader(\n", - " train_dataset, batch_size=args.batch_size, sampler=train_sampler, **kwargs)\n", - "\n", - "test_dataset = \\\n", - " datasets.MNIST('data-%d' % hvd.rank(), train=False, transform=transforms.Compose([\n", - " transforms.ToTensor(),\n", - " transforms.Normalize((0.1307,), (0.3081,))\n", - " ]))\n", - "test_sampler = torch.utils.data.distributed.DistributedSampler(\n", - " test_dataset, num_replicas=hvd.size(), rank=hvd.rank())\n", - "test_loader = torch.utils.data.DataLoader(test_dataset, batch_size=args.test_batch_size,\n", - " sampler=test_sampler, **kwargs)\n", - "\n", - "\n", - "class Net(nn.Module):\n", - " def __init__(self):\n", - " super(Net, self).__init__()\n", - " self.conv1 = nn.Conv2d(1, 10, kernel_size=5)\n", - " self.conv2 = nn.Conv2d(10, 20, kernel_size=5)\n", - " self.conv2_drop = nn.Dropout2d()\n", - " self.fc1 = nn.Linear(320, 50)\n", - " self.fc2 = nn.Linear(50, 10)\n", - "\n", - " def forward(self, x):\n", - " x = F.relu(F.max_pool2d(self.conv1(x), 2))\n", - " x = F.relu(F.max_pool2d(self.conv2_drop(self.conv2(x)), 2))\n", - " x = x.view(-1, 320)\n", - " x = F.relu(self.fc1(x))\n", - " x = F.dropout(x, training=self.training)\n", - " x = self.fc2(x)\n", - " return F.log_softmax(x)\n", - "\n", - "\n", - "model = Net()\n", - "\n", - "if args.cuda:\n", - " # Move model to GPU.\n", - " model.cuda()\n", - "\n", - "# Horovod: broadcast parameters.\n", - "hvd.broadcast_parameters(model.state_dict(), root_rank=0)\n", - "\n", - "# Horovod: scale learning rate by the number of GPUs.\n", - "optimizer = optim.SGD(model.parameters(), lr=args.lr * hvd.size(),\n", - " momentum=args.momentum)\n", - "\n", - "# Horovod: wrap optimizer with DistributedOptimizer.\n", - "optimizer = hvd.DistributedOptimizer(\n", - " optimizer, named_parameters=model.named_parameters())\n", - "\n", - "\n", - "def train(epoch):\n", - " model.train()\n", - " train_sampler.set_epoch(epoch)\n", - " for batch_idx, (data, target) in enumerate(train_loader):\n", - " if args.cuda:\n", - " data, target = data.cuda(), target.cuda()\n", - " data, target = Variable(data), Variable(target)\n", - " optimizer.zero_grad()\n", - " output = model(data)\n", - " loss = F.nll_loss(output, target)\n", - " loss.backward()\n", - " optimizer.step()\n", - " if batch_idx % args.log_interval == 0:\n", - " print('Train Epoch: {} [{}/{} ({:.0f}%)]\\tLoss: {:.6f}'.format(\n", - " epoch, batch_idx * len(data), len(train_sampler),\n", - " 100. * batch_idx / len(train_loader), loss.data[0]))\n", - "\n", - "\n", - "def metric_average(val, name):\n", - " tensor = torch.FloatTensor([val])\n", - " avg_tensor = hvd.allreduce(tensor, name=name)\n", - " return avg_tensor[0]\n", - "\n", - "\n", - "def test():\n", - " model.eval()\n", - " test_loss = 0.\n", - " test_accuracy = 0.\n", - " for data, target in test_loader:\n", - " if args.cuda:\n", - " data, target = data.cuda(), target.cuda()\n", - " data, target = Variable(data, volatile=True), Variable(target)\n", - " output = model(data)\n", - " # sum up batch loss\n", - " test_loss += F.nll_loss(output, target, size_average=False).data[0]\n", - " # get the index of the max log-probability\n", - " pred = output.data.max(1, keepdim=True)[1]\n", - " test_accuracy += pred.eq(target.data.view_as(pred)).cpu().float().sum()\n", - "\n", - " test_loss /= len(test_sampler)\n", - " test_accuracy /= len(test_sampler)\n", - "\n", - " test_loss = metric_average(test_loss, 'avg_loss')\n", - " test_accuracy = metric_average(test_accuracy, 'avg_accuracy')\n", - "\n", - " if hvd.rank() == 0:\n", - " print('\\nTest set: Average loss: {:.4f}, Accuracy: {:.2f}%\\n'.format(\n", - " test_loss, 100. * test_accuracy))\n", - "\n", - "\n", - "for epoch in range(1, args.epochs + 1):\n", - " train(epoch)\n", - " test()\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Deploy Batch AI cluster\n", - "\n", - "To run this in a distributed context, we'll need a Batch AI cluster with at least two nodes.\n", - "\n", - "Here, we use exactly two CPU nodes, to conserve resources. If you want to try it with some other number or SKU, just change the relevant values in the following code block." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from azureml.core.compute import BatchAiCompute\n", - "from azureml.core.compute import ComputeTarget\n", - "\n", - "batchai_cluster_name='gpucluster'\n", - "\n", - "\n", - "try:\n", - " # Check for existing cluster\n", - " compute_target = ComputeTarget(ws,batchai_cluster_name)\n", - " print('Found existing compute target')\n", - "except:\n", - " # Else, create new one\n", - " print('Creating a new compute target...')\n", - " provisioning_config = BatchAiCompute.provisioning_configuration(vm_size = \"STANDARD_NC6\", # NC6 is GPU-enabled\n", - " #vm_priority = 'lowpriority', # optional\n", - " autoscale_enabled = True,\n", - " cluster_min_nodes = 0, \n", - " cluster_max_nodes = 4)\n", - " compute_target = ComputeTarget.create(ws, batchai_cluster_name, provisioning_config)\n", - " # can poll for a minimum number of nodes and for a specific timeout. \n", - " # if no min node count is provided it will use the scale settings for the cluster\n", - " compute_target.wait_for_completion(show_output=True, min_node_count=None, timeout_in_minutes=20)\n", - "\n", - " # For a more detailed view of current BatchAI cluster status, use the 'status' property \n", - "print(compute_target.status.serialize())" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Submit job\n", - "\n", - "Now that we have a cluster ready to go, let's submit our job.\n", - "\n", - "We need to use a custom estimator here, and specify that we want the `pytorch`, `horovod` and `torchvision` packages installed to our image." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from azureml.train.dnn import PyTorch\n", - "\n", - "estimator = PyTorch(source_directory=project_folder,\n", - " compute_target=compute_target,\n", - " entry_script='pytorch_horovod_mnist.py',\n", - " node_count=2,\n", - " process_count_per_node=1,\n", - " distributed_backend=\"mpi\",\n", - " use_gpu=False)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from azureml.core.experiment import Experiment\n", - "\n", - "experiment = Experiment(workspace=ws, name=experiment_name)\n", - "run = experiment.submit(estimator)\n", - "print(run)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from azureml.train.widgets import RunDetails\n", - "RunDetails(run).show()" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.2" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/tutorials/01.train-models.ipynb b/tutorials/01.train-models.ipynb index e8a1e896b..916145955 100644 --- a/tutorials/01.train-models.ipynb +++ b/tutorials/01.train-models.ipynb @@ -121,7 +121,9 @@ "\n", "Azure Azure ML Managed Compute is a managed service that enables data scientists to train machine learning models on clusters of Azure virtual machines, including VMs with GPU support. In this tutorial, you create an Azure Managed Compute cluster as your training environment. This code creates a cluster for you if it does not already exist in your workspace. \n", "\n", - " **Creation of the cluster takes approximately 5 minutes.** If the cluster is already in the workspace this code uses it and skips the creation process." + " **Creation of the cluster takes approximately 5 minutes.** If the cluster is already in the workspace this code uses it and skips the creation process.\n", + "\n", + "**Note**: As with other Azure services, there are limits on certain resources (for eg. BatchAI cluster size) associated with the Azure Machine Learning service. Please read [this article](https://docs.microsoft.com/en-us/azure/machine-learning/service/how-to-manage-quotas) on the default limits and how to request more quota." ] }, {