From 057e22b253c630516072139c1511a47a7dd0135f Mon Sep 17 00:00:00 2001 From: vizhur Date: Mon, 13 Apr 2020 16:22:23 +0000 Subject: [PATCH] update samples from Release-6 as a part of 1.3.0 SDK stable release --- configuration.ipynb | 2 +- .../automated-machine-learning/README.md | 4 +- .../automated-machine-learning/automl_env.yml | 1 - .../automl_env_mac.yml | 3 +- ...fication-bank-marketing-all-features.ipynb | 59 +- ...sification-bank-marketing-all-features.yml | 2 - ...-ml-classification-credit-card-fraud.ipynb | 18 +- ...to-ml-classification-credit-card-fraud.yml | 1 - .../auto-ml-classification-text-dnn.ipynb | 65 ++- .../auto-ml-continuous-retraining.ipynb | 57 +- .../auto-ml-forecasting-beer-remote.ipynb | 32 +- .../auto-ml-forecasting-beer-remote.yml | 1 + .../auto-ml-forecasting-bike-share.ipynb | 55 +- .../auto-ml-forecasting-bike-share.yml | 1 + .../auto-ml-forecasting-energy-demand.ipynb | 58 +- .../auto-ml-forecasting-energy-demand.yml | 3 +- .../auto-ml-forecasting-function.ipynb | 53 +- .../auto-ml-forecasting-function.yml | 1 + .../forecast_function_at_train.png | Bin 0 -> 24461 bytes .../forecast_function_away_from_train.png | Bin 0 -> 24724 bytes ...to-ml-forecasting-orange-juice-sales.ipynb | 57 +- ...assification-credit-card-fraud-local.ipynb | 18 +- ...classification-credit-card-fraud-local.yml | 1 - ...egression-explanation-featurization.ipynb} | 61 +- ...l-regression-explanation-featurization.yml | 7 + .../score_explain.py | 0 .../train_explainer.py | 9 +- ...formance-explanation-and-featurization.yml | 10 - .../regression/auto-ml-regression.ipynb | 20 +- ...oml-databricks-local-with-deployment.ipynb | 183 ++---- .../multi-model-register-and-deploy.ipynb | 2 +- .../model-register-and-deploy.ipynb | 75 ++- .../onnx/onnx-model-register-and-deploy.ipynb | 2 +- .../production-deploy-to-aks-gpu.ipynb | 2 +- .../production-deploy-to-aks.ipynb | 68 ++- .../model-register-and-deploy-spark.ipynb | 2 +- ...tensorflow-model-register-and-deploy.ipynb | 2 +- .../explain-model-on-amlcompute.ipynb | 10 +- .../explain-model-on-amlcompute.yml | 2 + ...ve-retrieve-explanations-run-history.ipynb | 10 +- ...save-retrieve-explanations-run-history.yml | 2 + ...ain-explain-model-locally-and-deploy.ipynb | 10 +- ...train-explain-model-locally-and-deploy.yml | 2 + ...plain-model-on-amlcompute-and-deploy.ipynb | 13 +- ...explain-model-on-amlcompute-and-deploy.yml | 2 + ...with-automated-machine-learning-step.ipynb | 132 +++-- ...-taxi-data-regression-model-building.ipynb | 362 ++++-------- ...yc-taxi-data-regression-model-building.yml | 3 +- .../scripts/prepdata/cleanse.py | 40 +- .../scripts/prepdata/filter.py | 58 +- .../scripts/prepdata/merge.py | 29 +- .../scripts/prepdata/normalize.py | 53 +- .../scripts/prepdata/transform.py | 84 ++- .../scripts/trainmodel/featurization.py | 31 - .../scripts/trainmodel/get_data.py | 12 - .../scripts/trainmodel/train_test_split.py | 42 +- .../pipeline-style-transfer.ipynb | 12 +- ...erparameter-tune-deploy-with-chainer.ipynb | 9 + ...erparameter-tune-deploy-with-pytorch.ipynb | 9 + .../mask-rcnn-object-detection/coco_eval.py | 350 +++++++++++ .../mask-rcnn-object-detection/coco_utils.py | 252 ++++++++ .../mask-rcnn-object-detection/data.py | 77 +++ .../dockerfiles/Dockerfile | 16 + .../mask-rcnn-object-detection/engine.py | 108 ++++ .../mask-rcnn-object-detection/model.py | 23 + .../pytorch-mask-rcnn.ipynb | 544 ++++++++++++++++++ .../pytorch-mask-rcnn.yml | 14 + .../mask-rcnn-object-detection/script.py | 117 ++++ .../mask-rcnn-object-detection/transforms.py | 50 ++ .../mask-rcnn-object-detection/utils.py | 326 +++++++++++ ...erparameter-tune-deploy-with-sklearn.ipynb | 9 + ...arameter-tune-deploy-with-tensorflow.ipynb | 28 +- ...-tune-and-warm-start-with-tensorflow.ipynb | 28 +- .../train-tensorflow-resume-training.ipynb | 21 +- .../logging-api/logging-api.ipynb | 2 +- ...yperparameter-tune-deploy-with-keras.ipynb | 30 +- .../train-in-spark/train-in-spark.ipynb | 2 +- index.md | 3 +- setup-environment/configuration.ipynb | 2 +- .../imgs/experiment_main.png | Bin 64013 -> 69890 bytes .../imgs/model_download.png | Bin 19356 -> 26682 bytes .../tutorial-1st-experiment-sdk-train.ipynb | 22 +- .../img-classification-part2-deploy.ipynb | 387 +++++-------- 83 files changed, 3024 insertions(+), 1249 deletions(-) create mode 100644 how-to-use-azureml/automated-machine-learning/forecasting-high-frequency/forecast_function_at_train.png create mode 100644 how-to-use-azureml/automated-machine-learning/forecasting-high-frequency/forecast_function_away_from_train.png rename how-to-use-azureml/automated-machine-learning/{regression-hardware-performance-explanation-and-featurization/auto-ml-regression-hardware-performance-explanation-and-featurization.ipynb => regression-explanation-featurization/auto-ml-regression-explanation-featurization.ipynb} (95%) create mode 100644 how-to-use-azureml/automated-machine-learning/regression-explanation-featurization/auto-ml-regression-explanation-featurization.yml rename how-to-use-azureml/automated-machine-learning/{regression-hardware-performance-explanation-and-featurization => regression-explanation-featurization}/score_explain.py (100%) rename how-to-use-azureml/automated-machine-learning/{regression-hardware-performance-explanation-and-featurization => regression-explanation-featurization}/train_explainer.py (94%) delete mode 100644 how-to-use-azureml/automated-machine-learning/regression-hardware-performance-explanation-and-featurization/auto-ml-regression-hardware-performance-explanation-and-featurization.yml delete mode 100644 how-to-use-azureml/machine-learning-pipelines/nyc-taxi-data-regression-model-building/scripts/trainmodel/featurization.py delete mode 100644 how-to-use-azureml/machine-learning-pipelines/nyc-taxi-data-regression-model-building/scripts/trainmodel/get_data.py create mode 100644 how-to-use-azureml/ml-frameworks/pytorch/training/mask-rcnn-object-detection/coco_eval.py create mode 100644 how-to-use-azureml/ml-frameworks/pytorch/training/mask-rcnn-object-detection/coco_utils.py create mode 100644 how-to-use-azureml/ml-frameworks/pytorch/training/mask-rcnn-object-detection/data.py create mode 100644 how-to-use-azureml/ml-frameworks/pytorch/training/mask-rcnn-object-detection/dockerfiles/Dockerfile create mode 100644 how-to-use-azureml/ml-frameworks/pytorch/training/mask-rcnn-object-detection/engine.py create mode 100644 how-to-use-azureml/ml-frameworks/pytorch/training/mask-rcnn-object-detection/model.py create mode 100644 how-to-use-azureml/ml-frameworks/pytorch/training/mask-rcnn-object-detection/pytorch-mask-rcnn.ipynb create mode 100644 how-to-use-azureml/ml-frameworks/pytorch/training/mask-rcnn-object-detection/pytorch-mask-rcnn.yml create mode 100644 how-to-use-azureml/ml-frameworks/pytorch/training/mask-rcnn-object-detection/script.py create mode 100644 how-to-use-azureml/ml-frameworks/pytorch/training/mask-rcnn-object-detection/transforms.py create mode 100644 how-to-use-azureml/ml-frameworks/pytorch/training/mask-rcnn-object-detection/utils.py diff --git a/configuration.ipynb b/configuration.ipynb index 761aa8fd..91163dc8 100644 --- a/configuration.ipynb +++ b/configuration.ipynb @@ -103,7 +103,7 @@ "source": [ "import azureml.core\n", "\n", - "print(\"This notebook was created using version 1.2.0 of the Azure ML SDK\")\n", + "print(\"This notebook was created using version 1.3.0 of the Azure ML SDK\")\n", "print(\"You are currently using version\", azureml.core.VERSION, \"of the Azure ML SDK\")" ] }, diff --git a/how-to-use-azureml/automated-machine-learning/README.md b/how-to-use-azureml/automated-machine-learning/README.md index 94af87ae..ec5aa15a 100644 --- a/how-to-use-azureml/automated-machine-learning/README.md +++ b/how-to-use-azureml/automated-machine-learning/README.md @@ -117,7 +117,7 @@ jupyter notebook - Simple example of using automated ML for regression - Uses azure compute for training -- [auto-ml-regression-hardware-performance-explanation-and-featurization.ipynb](regression-hardware-performance-explanation-and-featurization/auto-ml-regression-hardware-performance-explanation-and-featurization.ipynb) +- [auto-ml-regression-explanation-featurization.ipynb](regression-explanation-featurization/auto-ml-regression-explanation-featurization.ipynb) - Dataset: Hardware Performance Dataset - Shows featurization and excplanation - Uses azure compute for training @@ -152,7 +152,7 @@ jupyter notebook - Beer Production Forecasting - [auto-ml-continuous-retraining.ipynb](continuous-retraining/auto-ml-continuous-retraining.ipynb) - - Continous retraining using Pipelines and Time-Series TabularDataset + - Continuous retraining using Pipelines and Time-Series TabularDataset - [auto-ml-classification-text-dnn.ipynb](classification-text-dnn/auto-ml-classification-text-dnn.ipynb) - Classification with text data using deep learning in AutoML diff --git a/how-to-use-azureml/automated-machine-learning/automl_env.yml b/how-to-use-azureml/automated-machine-learning/automl_env.yml index 569f205e..c8bfe39f 100644 --- a/how-to-use-azureml/automated-machine-learning/automl_env.yml +++ b/how-to-use-azureml/automated-machine-learning/automl_env.yml @@ -26,7 +26,6 @@ dependencies: - azureml-train - azureml-widgets - azureml-pipeline - - azureml-contrib-interpret - pytorch-transformers==1.0.0 - spacy==2.1.8 - onnxruntime==1.0.0 diff --git a/how-to-use-azureml/automated-machine-learning/automl_env_mac.yml b/how-to-use-azureml/automated-machine-learning/automl_env_mac.yml index 1027dc27..69749467 100644 --- a/how-to-use-azureml/automated-machine-learning/automl_env_mac.yml +++ b/how-to-use-azureml/automated-machine-learning/automl_env_mac.yml @@ -27,7 +27,6 @@ dependencies: - azureml-train - azureml-widgets - azureml-pipeline - - azureml-contrib-interpret - pytorch-transformers==1.0.0 - spacy==2.1.8 - onnxruntime==1.0.0 @@ -36,4 +35,4 @@ dependencies: channels: - anaconda - conda-forge -- pytorch \ No newline at end of file +- pytorch diff --git a/how-to-use-azureml/automated-machine-learning/classification-bank-marketing-all-features/auto-ml-classification-bank-marketing-all-features.ipynb b/how-to-use-azureml/automated-machine-learning/classification-bank-marketing-all-features/auto-ml-classification-bank-marketing-all-features.ipynb index 6a32356d..351332c0 100644 --- a/how-to-use-azureml/automated-machine-learning/classification-bank-marketing-all-features/auto-ml-classification-bank-marketing-all-features.ipynb +++ b/how-to-use-azureml/automated-machine-learning/classification-bank-marketing-all-features/auto-ml-classification-bank-marketing-all-features.ipynb @@ -92,6 +92,23 @@ "from azureml.explain.model._internal.explanation_client import ExplanationClient" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This sample notebook may use features that are not available in previous versions of the Azure ML SDK." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"This notebook was created using version 1.3.0 of the Azure ML SDK\")\n", + "print(\"You are currently using version\", azureml.core.VERSION, \"of the Azure ML SDK\")" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -132,7 +149,6 @@ "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'] = ws.name\n", "output['Resource Group'] = ws.resource_group\n", @@ -160,35 +176,22 @@ "metadata": {}, "outputs": [], "source": [ - "from azureml.core.compute import AmlCompute\n", - "from azureml.core.compute import ComputeTarget\n", + "from azureml.core.compute import ComputeTarget, AmlCompute\n", + "from azureml.core.compute_target import ComputeTargetException\n", "\n", - "# Choose a name for your cluster.\n", - "amlcompute_cluster_name = \"cpu-cluster-4\"\n", + "# Choose a name for your CPU cluster\n", + "cpu_cluster_name = \"cpu-cluster-4\"\n", "\n", - "found = False\n", - "# Check if this compute target already exists in the workspace.\n", - "cts = ws.compute_targets\n", - "if amlcompute_cluster_name in cts and cts[amlcompute_cluster_name].type == 'AmlCompute':\n", - " found = True\n", - " print('Found existing compute target.')\n", - " compute_target = cts[amlcompute_cluster_name]\n", - " \n", - "if not found:\n", - " print('Creating a new compute target...')\n", - " provisioning_config = AmlCompute.provisioning_configuration(vm_size = \"STANDARD_D2_V2\", # for GPU, use \"STANDARD_NC6\"\n", - " #vm_priority = 'lowpriority', # optional\n", - " max_nodes = 6)\n", + "# Verify that cluster does not exist already\n", + "try:\n", + " compute_target = ComputeTarget(workspace=ws, name=cpu_cluster_name)\n", + " print('Found existing cluster, use it.')\n", + "except ComputeTargetException:\n", + " compute_config = AmlCompute.provisioning_configuration(vm_size='STANDARD_D2_V2',\n", + " max_nodes=6)\n", + " compute_target = ComputeTarget.create(ws, cpu_cluster_name, compute_config)\n", "\n", - " # Create the cluster.\n", - " compute_target = ComputeTarget.create(ws, amlcompute_cluster_name, provisioning_config)\n", - " \n", - "print('Checking cluster status...')\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 AmlCompute status, use get_status()." + "compute_target.wait_for_completion(show_output=True)" ] }, { @@ -394,8 +397,6 @@ "outputs": [], "source": [ "#from azureml.train.automl.run import AutoMLRun\n", - "#experiment_name = 'automl-classification-bmarketing'\n", - "#experiment = Experiment(ws, experiment_name)\n", "#remote_run = AutoMLRun(experiment=experiment, run_id='7n-yR!RsWN|Df%-XpyRP$3`? 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') # this name is model.id of model that we want to deploy\n", - " # deserialize the model file back into a sklearn model\n", - " model = joblib.load(model_path)\n", - "\n", - "def run(raw_data):\n", - " try:\n", - " data = (pd.DataFrame(np.array(json.loads(raw_data)['data']), columns=[str(i) for i in range(0,64)]))\n", - " result = model.predict(data)\n", - " except Exception as e:\n", - " result = str(e)\n", - " return json.dumps({\"error\": result})\n", - " return json.dumps({\"result\":result.tolist()})" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#Replace <>\n", - "content = \"\"\n", - "with open(\"score.py\", \"r\") as fo:\n", - " content = fo.read()\n", - "\n", - "new_content = content.replace(\"<>\", local_run.model_id)\n", - "with open(\"score.py\", \"w\") as fw:\n", - " fw.write(new_content)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Create a YAML File for the Environment" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from azureml.core.conda_dependencies import CondaDependencies\n", - "\n", - "myenv = CondaDependencies.create(conda_packages=['numpy','scikit-learn'], pip_packages=['azureml-defaults', 'azureml-sdk[automl]'])\n", - "\n", - "conda_env_file_name = 'myenv.yml'\n", - "myenv.save_to_file('.', conda_env_file_name)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Deploy the model as a Web Service on Azure Container Instance\n", - "Replace servicename with any meaningful name of service" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# this will take 10-15 minutes to finish\n", - "\n", - "from azureml.core.webservice import AciWebservice, Webservice\n", - "from azureml.exceptions import WebserviceException\n", "from azureml.core.model import InferenceConfig\n", - "from azureml.core.model import Model\n", + "from azureml.core.webservice import AciWebservice\n", "from azureml.core.environment import Environment\n", - "from azureml.core.conda_dependencies import CondaDependencies\n", - "import uuid\n", "\n", + "myenv = Environment.from_conda_specification(name=\"myenv\", file_path=conda_env_file_name)\n", + "inference_config = InferenceConfig(entry_script=script_file_name, environment=myenv)\n", "\n", - "myaci_config = AciWebservice.deploy_configuration(\n", - " cpu_cores = 2, \n", - " memory_gb = 2, \n", - " tags = {'name':'Databricks Azure ML ACI'}, \n", - " description = 'This is for ADB and AutoML example.')\n", - "\n", - "myenv = Environment.get(ws, name='AzureML-PySpark-MmlSpark-0.15')\n", - "# we need to add extra packages to procured environment\n", - "# in order to deploy amended environment we need to rename it\n", - "myenv.name = 'myenv'\n", - "model_dependencies = CondaDependencies('myenv.yml')\n", - "for pip_dep in model_dependencies.pip_packages:\n", - " myenv.python.conda_dependencies.add_pip_package(pip_dep)\n", - "for conda_dep in model_dependencies.conda_packages:\n", - " myenv.python.conda_dependencies.add_conda_package(conda_dep)\n", - "inference_config = InferenceConfig(entry_script='score_sparkml.py', environment=myenv)\n", - "\n", - "guid = str(uuid.uuid4()).split(\"-\")[0]\n", - "service_name = \"myservice-{}\".format(guid)\n", - "\n", - "# Remove any existing service under the same name.\n", - "try:\n", - " Webservice(ws, service_name).delete()\n", - "except WebserviceException:\n", - " pass\n", - "\n", - "print(\"Creating service with name: {}\".format(service_name))\n", - "\n", - "myservice = Model.deploy(ws, service_name, [model], inference_config, myaci_config)\n", - "myservice.wait_for_deployment(show_output=True)" + "aciconfig = AciWebservice.deploy_configuration(cpu_cores = 1, \n", + " memory_gb = 1, \n", + " tags = {'area': \"digits\", 'type': \"automl_classification\"}, \n", + " description = 'sample service for Automl Classification')" ] }, { @@ -707,8 +626,14 @@ "metadata": {}, "outputs": [], "source": [ - "#for using the Web HTTP API \n", - "print(myservice.scoring_uri)" + "from azureml.core.webservice import Webservice\n", + "from azureml.core.model import Model\n", + "\n", + "aci_service_name = 'automl-databricks-local'\n", + "print(aci_service_name)\n", + "aci_service = Model.deploy(ws, aci_service_name, [model], inference_config, aciconfig)\n", + "aci_service.wait_for_deployment(True)\n", + "print(aci_service.state)" ] }, { @@ -752,7 +677,7 @@ "for index in np.random.choice(len(y_test), 2, replace = False):\n", " print(index)\n", " test_sample = json.dumps({'data':X_test[index:index + 1].values.tolist()})\n", - " predicted = myservice.run(input_data = test_sample)\n", + " predicted = aci_service.run(input_data = test_sample)\n", " label = y_test.values[index]\n", " predictedDict = json.loads(predicted)\n", " title = \"Label value = %d Predicted value = %s \" % ( label,predictedDict['result'][0]) \n", diff --git a/how-to-use-azureml/deployment/deploy-multi-model/multi-model-register-and-deploy.ipynb b/how-to-use-azureml/deployment/deploy-multi-model/multi-model-register-and-deploy.ipynb index 7c3d7ed7..1fbc5511 100644 --- a/how-to-use-azureml/deployment/deploy-multi-model/multi-model-register-and-deploy.ipynb +++ b/how-to-use-azureml/deployment/deploy-multi-model/multi-model-register-and-deploy.ipynb @@ -285,7 +285,7 @@ "from azureml.exceptions import WebserviceException\n", "\n", "deployment_config = AciWebservice.deploy_configuration(cpu_cores=1, memory_gb=1)\n", - "aci_service_name = 'aciservice1'\n", + "aci_service_name = 'aciservice-multimodel'\n", "\n", "try:\n", " # if you want to get existing service below is the command\n", diff --git a/how-to-use-azureml/deployment/deploy-to-cloud/model-register-and-deploy.ipynb b/how-to-use-azureml/deployment/deploy-to-cloud/model-register-and-deploy.ipynb index 44bfde49..6c81ead9 100644 --- a/how-to-use-azureml/deployment/deploy-to-cloud/model-register-and-deploy.ipynb +++ b/how-to-use-azureml/deployment/deploy-to-cloud/model-register-and-deploy.ipynb @@ -388,6 +388,14 @@ "Below is an example of how you can construct an input dataset to profile a service which expects its incoming requests to contain serialized json. In this case we created a dataset based one hundred instances of the same request data. In real world scenarios however, we suggest that you use larger datasets with various inputs, especially if your model resource usage/behavior is input dependent." ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You may want to register datasets using the register() method to your workspace so they can be shared with others, reused and referred to by name in your script.\n", + "You can try get the dataset first to see if it's already registered." + ] + }, { "cell_type": "code", "execution_count": null, @@ -398,36 +406,45 @@ "from azureml.core.dataset import Dataset\n", "from azureml.data import dataset_type_definitions\n", "\n", + "dataset_name='diabetes_sample_request_data'\n", "\n", - "# create a string that can be utf-8 encoded and\n", - "# put in the body of the request\n", - "serialized_input_json = json.dumps({\n", - " 'data': [\n", - " [ 0.03807591, 0.05068012, 0.06169621, 0.02187235, -0.0442235,\n", - " -0.03482076, -0.04340085, -0.00259226, 0.01990842, -0.01764613]\n", - " ]\n", - "})\n", - "dataset_content = []\n", - "for i in range(100):\n", - " dataset_content.append(serialized_input_json)\n", - "dataset_content = '\\n'.join(dataset_content)\n", - "file_name = 'sample_request_data.txt'\n", - "f = open(file_name, 'w')\n", - "f.write(dataset_content)\n", - "f.close()\n", + "dataset_registered = False\n", + "try:\n", + " sample_request_data = Dataset.get_by_name(workspace = ws, name = dataset_name)\n", + " dataset_registered = True\n", + "except:\n", + " print(\"The dataset {} is not registered in workspace yet.\".format(dataset_name))\n", "\n", - "# upload the txt file created above to the Datastore and create a dataset from it\n", - "data_store = Datastore.get_default(ws)\n", - "data_store.upload_files(['./' + file_name], target_path='sample_request_data')\n", - "datastore_path = [(data_store, 'sample_request_data' +'/' + file_name)]\n", - "sample_request_data = Dataset.Tabular.from_delimited_files(\n", - " datastore_path,\n", - " separator='\\n',\n", - " infer_column_types=True,\n", - " header=dataset_type_definitions.PromoteHeadersBehavior.NO_HEADERS)\n", - "sample_request_data = sample_request_data.register(workspace=ws,\n", - " name='diabetes_sample_request_data',\n", - " create_new_version=True)" + "if not dataset_registered:\n", + " # create a string that can be utf-8 encoded and\n", + " # put in the body of the request\n", + " serialized_input_json = json.dumps({\n", + " 'data': [\n", + " [ 0.03807591, 0.05068012, 0.06169621, 0.02187235, -0.0442235,\n", + " -0.03482076, -0.04340085, -0.00259226, 0.01990842, -0.01764613]\n", + " ]\n", + " })\n", + " dataset_content = []\n", + " for i in range(100):\n", + " dataset_content.append(serialized_input_json)\n", + " dataset_content = '\\n'.join(dataset_content)\n", + " file_name = \"{}.txt\".format(dataset_name)\n", + " f = open(file_name, 'w')\n", + " f.write(dataset_content)\n", + " f.close()\n", + "\n", + " # upload the txt file created above to the Datastore and create a dataset from it\n", + " data_store = Datastore.get_default(ws)\n", + " data_store.upload_files(['./' + file_name], target_path='sample_request_data')\n", + " datastore_path = [(data_store, 'sample_request_data' +'/' + file_name)]\n", + " sample_request_data = Dataset.Tabular.from_delimited_files(\n", + " datastore_path,\n", + " separator='\\n',\n", + " infer_column_types=True,\n", + " header=dataset_type_definitions.PromoteHeadersBehavior.NO_HEADERS)\n", + " sample_request_data = sample_request_data.register(workspace=ws,\n", + " name=dataset_name,\n", + " create_new_version=True)" ] }, { @@ -512,7 +529,7 @@ "metadata": { "authors": [ { - "name": "aashishb" + "name": "vaidyas" } ], "category": "deployment", diff --git a/how-to-use-azureml/deployment/onnx/onnx-model-register-and-deploy.ipynb b/how-to-use-azureml/deployment/onnx/onnx-model-register-and-deploy.ipynb index f0334e85..42fdb6d8 100644 --- a/how-to-use-azureml/deployment/onnx/onnx-model-register-and-deploy.ipynb +++ b/how-to-use-azureml/deployment/onnx/onnx-model-register-and-deploy.ipynb @@ -202,7 +202,7 @@ "metadata": { "authors": [ { - "name": "aashishb" + "name": "vaidyas" } ], "kernelspec": { diff --git a/how-to-use-azureml/deployment/production-deploy-to-aks-gpu/production-deploy-to-aks-gpu.ipynb b/how-to-use-azureml/deployment/production-deploy-to-aks-gpu/production-deploy-to-aks-gpu.ipynb index adbd1f3e..aed26009 100644 --- a/how-to-use-azureml/deployment/production-deploy-to-aks-gpu/production-deploy-to-aks-gpu.ipynb +++ b/how-to-use-azureml/deployment/production-deploy-to-aks-gpu/production-deploy-to-aks-gpu.ipynb @@ -288,7 +288,7 @@ "metadata": { "authors": [ { - "name": "aashishb" + "name": "vaidyas" } ], "kernelspec": { diff --git a/how-to-use-azureml/deployment/production-deploy-to-aks/production-deploy-to-aks.ipynb b/how-to-use-azureml/deployment/production-deploy-to-aks/production-deploy-to-aks.ipynb index 8878db80..5ea43c86 100644 --- a/how-to-use-azureml/deployment/production-deploy-to-aks/production-deploy-to-aks.ipynb +++ b/how-to-use-azureml/deployment/production-deploy-to-aks/production-deploy-to-aks.ipynb @@ -217,6 +217,14 @@ "Below is an example of how you can construct an input dataset to profile a service which expects its incoming requests to contain serialized json. In this case we created a dataset based one hundred instances of the same request data. In real world scenarios however, we suggest that you use larger datasets with various inputs, especially if your model resource usage/behavior is input dependent." ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You may want to register datasets using the register() method to your workspace so they can be shared with others, reused and referred to by name in your script.\n", + "You can try get the dataset first to see if it's already registered." + ] + }, { "cell_type": "code", "execution_count": null, @@ -228,31 +236,41 @@ "from azureml.core.dataset import Dataset\n", "from azureml.data import dataset_type_definitions\n", "\n", - "input_json = {'data': [[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],\n", - " [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]]}\n", - "# create a string that can be put in the body of the request\n", - "serialized_input_json = json.dumps(input_json)\n", - "dataset_content = []\n", - "for i in range(100):\n", - " dataset_content.append(serialized_input_json)\n", - "sample_request_data = '\\n'.join(dataset_content)\n", - "file_name = 'sample_request_data.txt'\n", - "f = open(file_name, 'w')\n", - "f.write(sample_request_data)\n", - "f.close()\n", + "dataset_name='sample_request_data'\n", "\n", - "# upload the txt file created above to the Datastore and create a dataset from it\n", - "data_store = Datastore.get_default(ws)\n", - "data_store.upload_files(['./' + file_name], target_path='sample_request_data')\n", - "datastore_path = [(data_store, 'sample_request_data' +'/' + file_name)]\n", - "sample_request_data = Dataset.Tabular.from_delimited_files(\n", - " datastore_path,\n", - " separator='\\n',\n", - " infer_column_types=True,\n", - " header=dataset_type_definitions.PromoteHeadersBehavior.NO_HEADERS)\n", - "sample_request_data = sample_request_data.register(workspace=ws,\n", - " name='sample_request_data',\n", - " create_new_version=True)" + "dataset_registered = False\n", + "try:\n", + " sample_request_data = Dataset.get_by_name(workspace = ws, name = dataset_name)\n", + " dataset_registered = True\n", + "except:\n", + " print(\"The dataset {} is not registered in workspace yet.\".format(dataset_name))\n", + "\n", + "if not dataset_registered:\n", + " input_json = {'data': [[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],\n", + " [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]]}\n", + " # create a string that can be put in the body of the request\n", + " serialized_input_json = json.dumps(input_json)\n", + " dataset_content = []\n", + " for i in range(100):\n", + " dataset_content.append(serialized_input_json)\n", + " sample_request_data = '\\n'.join(dataset_content)\n", + " file_name = \"{}.txt\".format(dataset_name)\n", + " f = open(file_name, 'w')\n", + " f.write(sample_request_data)\n", + " f.close()\n", + "\n", + " # upload the txt file created above to the Datastore and create a dataset from it\n", + " data_store = Datastore.get_default(ws)\n", + " data_store.upload_files(['./' + file_name], target_path='sample_request_data')\n", + " datastore_path = [(data_store, 'sample_request_data' +'/' + file_name)]\n", + " sample_request_data = Dataset.Tabular.from_delimited_files(\n", + " datastore_path,\n", + " separator='\\n',\n", + " infer_column_types=True,\n", + " header=dataset_type_definitions.PromoteHeadersBehavior.NO_HEADERS)\n", + " sample_request_data = sample_request_data.register(workspace=ws,\n", + " name=dataset_name,\n", + " create_new_version=True)" ] }, { @@ -560,7 +578,7 @@ "metadata": { "authors": [ { - "name": "aashishb" + "name": "vaidyas" } ], "kernelspec": { diff --git a/how-to-use-azureml/deployment/spark/model-register-and-deploy-spark.ipynb b/how-to-use-azureml/deployment/spark/model-register-and-deploy-spark.ipynb index 90c0c278..a254f898 100644 --- a/how-to-use-azureml/deployment/spark/model-register-and-deploy-spark.ipynb +++ b/how-to-use-azureml/deployment/spark/model-register-and-deploy-spark.ipynb @@ -302,7 +302,7 @@ "metadata": { "authors": [ { - "name": "aashishb" + "name": "vaidyas" } ], "category": "deployment", diff --git a/how-to-use-azureml/deployment/tensorflow/tensorflow-model-register-and-deploy.ipynb b/how-to-use-azureml/deployment/tensorflow/tensorflow-model-register-and-deploy.ipynb index 10a33aa5..7cdbcca4 100644 --- a/how-to-use-azureml/deployment/tensorflow/tensorflow-model-register-and-deploy.ipynb +++ b/how-to-use-azureml/deployment/tensorflow/tensorflow-model-register-and-deploy.ipynb @@ -234,7 +234,7 @@ "metadata": { "authors": [ { - "name": "aashishb" + "name": "vaidyas" } ], "kernelspec": { diff --git a/how-to-use-azureml/explain-model/azure-integration/remote-explanation/explain-model-on-amlcompute.ipynb b/how-to-use-azureml/explain-model/azure-integration/remote-explanation/explain-model-on-amlcompute.ipynb index b444686b..2cae8957 100644 --- a/how-to-use-azureml/explain-model/azure-integration/remote-explanation/explain-model-on-amlcompute.ipynb +++ b/how-to-use-azureml/explain-model/azure-integration/remote-explanation/explain-model-on-amlcompute.ipynb @@ -687,12 +687,12 @@ "source": [ "## Next\n", "Learn about other use cases of the explain package on a:\n", - "1. [Training time: regression problem](../../tabular-data/explain-binary-classification-local.ipynb) \n", - "1. [Training time: binary classification problem](../../tabular-data/explain-binary-classification-local.ipynb)\n", - "1. [Training time: multiclass classification problem](../../tabular-data/explain-multiclass-classification-local.ipynb)\n", + "1. [Training time: regression problem](https://github.com/interpretml/interpret-community/blob/master/notebooks/explain-regression-local.ipynb) \n", + "1. [Training time: binary classification problem](https://github.com/interpretml/interpret-community/blob/master/notebooks/explain-binary-classification-local.ipynb)\n", + "1. [Training time: multiclass classification problem](https://github.com/interpretml/interpret-community/blob/master/notebooks/explain-multiclass-classification-local.ipynb)\n", "1. Explain models with engineered features:\n", - " 1. [Simple feature transformations](../../tabular-data/simple-feature-transformations-explain-local.ipynb)\n", - " 1. [Advanced feature transformations](../../tabular-data/advanced-feature-transformations-explain-local.ipynb)\n", + " 1. [Simple feature transformations](https://github.com/interpretml/interpret-community/blob/master/notebooks/simple-feature-transformations-explain-local.ipynb)\n", + " 1. [Advanced feature transformations](https://github.com/interpretml/interpret-community/blob/master/notebooks/advanced-feature-transformations-explain-local.ipynb)\n", "1. [Save model explanations via Azure Machine Learning Run History](../run-history/save-retrieve-explanations-run-history.ipynb)\n", "1. Inferencing time: deploy a classification model and explainer:\n", " 1. [Deploy a locally-trained model and explainer](../scoring-time/train-explain-model-locally-and-deploy.ipynb)\n", diff --git a/how-to-use-azureml/explain-model/azure-integration/remote-explanation/explain-model-on-amlcompute.yml b/how-to-use-azureml/explain-model/azure-integration/remote-explanation/explain-model-on-amlcompute.yml index aaa7070f..7a4b22f7 100644 --- a/how-to-use-azureml/explain-model/azure-integration/remote-explanation/explain-model-on-amlcompute.yml +++ b/how-to-use-azureml/explain-model/azure-integration/remote-explanation/explain-model-on-amlcompute.yml @@ -3,6 +3,8 @@ dependencies: - pip: - azureml-sdk - azureml-interpret + - interpret-community[visualization] + - matplotlib - azureml-contrib-interpret - sklearn-pandas - azureml-dataprep diff --git a/how-to-use-azureml/explain-model/azure-integration/run-history/save-retrieve-explanations-run-history.ipynb b/how-to-use-azureml/explain-model/azure-integration/run-history/save-retrieve-explanations-run-history.ipynb index 1343d51b..60dca68d 100644 --- a/how-to-use-azureml/explain-model/azure-integration/run-history/save-retrieve-explanations-run-history.ipynb +++ b/how-to-use-azureml/explain-model/azure-integration/run-history/save-retrieve-explanations-run-history.ipynb @@ -582,12 +582,12 @@ "source": [ "## Next\n", "Learn about other use cases of the explain package on a:\n", - "1. [Training time: regression problem](../../tabular-data/explain-binary-classification-local.ipynb) \n", - "1. [Training time: binary classification problem](../../tabular-data/explain-binary-classification-local.ipynb)\n", - "1. [Training time: multiclass classification problem](../../tabular-data/explain-multiclass-classification-local.ipynb)\n", + "1. [Training time: regression problem](https://github.com/interpretml/interpret-community/blob/master/notebooks/explain-regression-local.ipynb) \n", + "1. [Training time: binary classification problem](https://github.com/interpretml/interpret-community/blob/master/notebooks/explain-binary-classification-local.ipynb)\n", + "1. [Training time: multiclass classification problem](https://github.com/interpretml/interpret-community/blob/master/notebooks/explain-multiclass-classification-local.ipynb)\n", "1. Explain models with engineered features:\n", - " 1. [Simple feature transformations](../../tabular-data/simple-feature-transformations-explain-local.ipynb)\n", - " 1. [Advanced feature transformations](../../tabular-data/advanced-feature-transformations-explain-local.ipynb)\n", + " 1. [Simple feature transformations](https://github.com/interpretml/interpret-community/blob/master/notebooks/simple-feature-transformations-explain-local.ipynb)\n", + " 1. [Advanced feature transformations](https://github.com/interpretml/interpret-community/blob/master/notebooks/advanced-feature-transformations-explain-local.ipynb)\n", "1. [Run explainers remotely on Azure Machine Learning Compute (AMLCompute)](../remote-explanation/explain-model-on-amlcompute.ipynb)\n", "1. Inferencing time: deploy a classification model and explainer:\n", " 1. [Deploy a locally-trained model and explainer](../scoring-time/train-explain-model-locally-and-deploy.ipynb)\n", diff --git a/how-to-use-azureml/explain-model/azure-integration/run-history/save-retrieve-explanations-run-history.yml b/how-to-use-azureml/explain-model/azure-integration/run-history/save-retrieve-explanations-run-history.yml index 2dee986f..ff76d75f 100644 --- a/how-to-use-azureml/explain-model/azure-integration/run-history/save-retrieve-explanations-run-history.yml +++ b/how-to-use-azureml/explain-model/azure-integration/run-history/save-retrieve-explanations-run-history.yml @@ -3,5 +3,7 @@ dependencies: - pip: - azureml-sdk - azureml-interpret + - interpret-community[visualization] + - matplotlib - azureml-contrib-interpret - ipywidgets diff --git a/how-to-use-azureml/explain-model/azure-integration/scoring-time/train-explain-model-locally-and-deploy.ipynb b/how-to-use-azureml/explain-model/azure-integration/scoring-time/train-explain-model-locally-and-deploy.ipynb index 24b34c52..7a1af029 100644 --- a/how-to-use-azureml/explain-model/azure-integration/scoring-time/train-explain-model-locally-and-deploy.ipynb +++ b/how-to-use-azureml/explain-model/azure-integration/scoring-time/train-explain-model-locally-and-deploy.ipynb @@ -445,12 +445,12 @@ "source": [ "## Next\n", "Learn about other use cases of the explain package on a:\n", - "1. [Training time: regression problem](../../tabular-data/explain-binary-classification-local.ipynb) \n", - "1. [Training time: binary classification problem](../../tabular-data/explain-binary-classification-local.ipynb)\n", - "1. [Training time: multiclass classification problem](../../tabular-data/explain-multiclass-classification-local.ipynb)\n", + "1. [Training time: regression problem](https://github.com/interpretml/interpret-community/blob/master/notebooks/explain-regression-local.ipynb) \n", + "1. [Training time: binary classification problem](https://github.com/interpretml/interpret-community/blob/master/notebooks/explain-binary-classification-local.ipynb)\n", + "1. [Training time: multiclass classification problem](https://github.com/interpretml/interpret-community/blob/master/notebooks/explain-multiclass-classification-local.ipynb)\n", "1. Explain models with engineered features:\n", - " 1. [Simple feature transformations](../../tabular-data/simple-feature-transformations-explain-local.ipynb)\n", - " 1. [Advanced feature transformations](../../tabular-data/advanced-feature-transformations-explain-local.ipynb)\n", + " 1. [Simple feature transformations](https://github.com/interpretml/interpret-community/blob/master/notebooks/simple-feature-transformations-explain-local.ipynb)\n", + " 1. [Advanced feature transformations](https://github.com/interpretml/interpret-community/blob/master/notebooks/advanced-feature-transformations-explain-local.ipynb)\n", "1. [Save model explanations via Azure Machine Learning Run History](../run-history/save-retrieve-explanations-run-history.ipynb)\n", "1. [Run explainers remotely on Azure Machine Learning Compute (AMLCompute)](../remote-explanation/explain-model-on-amlcompute.ipynb)\n", "1. [Inferencing time: deploy a remotely-trained model and explainer](./train-explain-model-on-amlcompute-and-deploy.ipynb)" diff --git a/how-to-use-azureml/explain-model/azure-integration/scoring-time/train-explain-model-locally-and-deploy.yml b/how-to-use-azureml/explain-model/azure-integration/scoring-time/train-explain-model-locally-and-deploy.yml index 7236444a..b7a4cd6f 100644 --- a/how-to-use-azureml/explain-model/azure-integration/scoring-time/train-explain-model-locally-and-deploy.yml +++ b/how-to-use-azureml/explain-model/azure-integration/scoring-time/train-explain-model-locally-and-deploy.yml @@ -3,6 +3,8 @@ dependencies: - pip: - azureml-sdk - azureml-interpret + - interpret-community[visualization] + - matplotlib - azureml-contrib-interpret - sklearn-pandas - ipywidgets diff --git a/how-to-use-azureml/explain-model/azure-integration/scoring-time/train-explain-model-on-amlcompute-and-deploy.ipynb b/how-to-use-azureml/explain-model/azure-integration/scoring-time/train-explain-model-on-amlcompute-and-deploy.ipynb index a8f8b88f..946eef5c 100644 --- a/how-to-use-azureml/explain-model/azure-integration/scoring-time/train-explain-model-on-amlcompute-and-deploy.ipynb +++ b/how-to-use-azureml/explain-model/azure-integration/scoring-time/train-explain-model-on-amlcompute-and-deploy.ipynb @@ -483,16 +483,15 @@ "source": [ "## Next\n", "Learn about other use cases of the explain package on a:\n", - "1. [Training time: regression problem](../../tabular-data/explain-binary-classification-local.ipynb) \n", - "1. [Training time: binary classification problem](../../tabular-data/explain-binary-classification-local.ipynb)\n", - "1. [Training time: multiclass classification problem](../../tabular-data/explain-multiclass-classification-local.ipynb)\n", + "1. [Training time: regression problem](https://github.com/interpretml/interpret-community/blob/master/notebooks/explain-regression-local.ipynb) \n", + "1. [Training time: binary classification problem](https://github.com/interpretml/interpret-community/blob/master/notebooks/explain-binary-classification-local.ipynb)\n", + "1. [Training time: multiclass classification problem](https://github.com/interpretml/interpret-community/blob/master/notebooks/explain-multiclass-classification-local.ipynb)\n", "1. Explain models with engineered features:\n", - " 1. [Simple feature transformations](../../tabular-data/simple-feature-transformations-explain-local.ipynb)\n", - " 1. [Advanced feature transformations](../../tabular-data/advanced-feature-transformations-explain-local.ipynb)\n", + " 1. [Simple feature transformations](https://github.com/interpretml/interpret-community/blob/master/notebooks/simple-feature-transformations-explain-local.ipynb)\n", + " 1. [Advanced feature transformations](https://github.com/interpretml/interpret-community/blob/master/notebooks/advanced-feature-transformations-explain-local.ipynb)\n", "1. [Save model explanations via Azure Machine Learning Run History](../run-history/save-retrieve-explanations-run-history.ipynb)\n", "1. [Run explainers remotely on Azure Machine Learning Compute (AMLCompute)](../remote-explanation/explain-model-on-amlcompute.ipynb)\n", - "1. [Inferencing time: deploy a locally-trained model and explainer](./train-explain-model-locally-and-deploy.ipynb)\n", - " " + "1. [Inferencing time: deploy a locally-trained model and explainer](./train-explain-model-locally-and-deploy.ipynb)" ] }, { diff --git a/how-to-use-azureml/explain-model/azure-integration/scoring-time/train-explain-model-on-amlcompute-and-deploy.yml b/how-to-use-azureml/explain-model/azure-integration/scoring-time/train-explain-model-on-amlcompute-and-deploy.yml index ab91cef6..ab5f0f94 100644 --- a/how-to-use-azureml/explain-model/azure-integration/scoring-time/train-explain-model-on-amlcompute-and-deploy.yml +++ b/how-to-use-azureml/explain-model/azure-integration/scoring-time/train-explain-model-on-amlcompute-and-deploy.yml @@ -3,6 +3,8 @@ dependencies: - pip: - azureml-sdk - azureml-interpret + - interpret-community[visualization] + - matplotlib - azureml-contrib-interpret - sklearn-pandas - azureml-dataprep diff --git a/how-to-use-azureml/machine-learning-pipelines/intro-to-pipelines/aml-pipelines-with-automated-machine-learning-step.ipynb b/how-to-use-azureml/machine-learning-pipelines/intro-to-pipelines/aml-pipelines-with-automated-machine-learning-step.ipynb index 7b1017c6..34a2a723 100644 --- a/how-to-use-azureml/machine-learning-pipelines/intro-to-pipelines/aml-pipelines-with-automated-machine-learning-step.ipynb +++ b/how-to-use-azureml/machine-learning-pipelines/intro-to-pipelines/aml-pipelines-with-automated-machine-learning-step.ipynb @@ -105,7 +105,7 @@ "metadata": {}, "source": [ "## Create an Azure ML experiment\n", - "Let's create an experiment named \"automl-classification\" and a folder to hold the training scripts. The script runs will be recorded under the experiment in Azure.\n", + "Let's create an experiment named \"automlstep-classification\" and a folder to hold the training scripts. The script runs will be recorded under the experiment in Azure.\n", "\n", "The best practice is to use separate folders for scripts and its dependent files for each step and specify that folder as the `source_directory` for the step. This helps reduce the size of the snapshot created for the step (only the specific folder is snapshotted). Since changes in any files in the `source_directory` would trigger a re-upload of the snapshot, this helps keep the reuse of the step when there are no changes in the `source_directory` of the step." ] @@ -165,20 +165,6 @@ " # For a more detailed view of current AmlCompute status, use get_status()." ] }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# create a new RunConfig object\n", - "conda_run_config = RunConfiguration(framework=\"python\")\n", - "cd = CondaDependencies.create(pip_packages=['azureml-sdk[automl]'])\n", - "conda_run_config.environment.python.conda_dependencies = cd\n", - "\n", - "print('run config is ready')" - ] - }, { "cell_type": "markdown", "metadata": {}, @@ -192,19 +178,30 @@ "metadata": {}, "outputs": [], "source": [ - "# The data referenced here was a 1MB simple random sample of the Chicago Crime data into a local temporary directory.\n", - "example_data = 'https://dprepdata.blob.core.windows.net/demo/crime0-random.csv'\n", - "dataset = Dataset.Tabular.from_delimited_files(example_data)\n", - "dataset.to_pandas_dataframe().describe()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "dataset.take(5).to_pandas_dataframe()" + "# Try to load the dataset from the Workspace. Otherwise, create it from the file\n", + "found = False\n", + "key = \"Crime-Dataset\"\n", + "description_text = \"Crime Dataset (used in the the aml-pipelines-with-automated-machine-learning-step.ipynb notebook)\"\n", + "\n", + "if key in ws.datasets.keys(): \n", + " found = True\n", + " dataset = ws.datasets[key] \n", + "\n", + "if not found:\n", + " # Create AML Dataset and register it into Workspace\n", + " # The data referenced here was a 1MB simple random sample of the Chicago Crime data into a local temporary directory.\n", + " example_data = 'https://dprepdata.blob.core.windows.net/demo/crime0-random.csv'\n", + " dataset = Dataset.Tabular.from_delimited_files(example_data)\n", + " dataset = dataset.drop_columns(['FBI Code'])\n", + " \n", + " #Register Dataset in Workspace\n", + " dataset = dataset.register(workspace=ws,\n", + " name=key,\n", + " description=description_text)\n", + "\n", + "\n", + "df = dataset.to_pandas_dataframe()\n", + "df.describe()" ] }, { @@ -224,9 +221,7 @@ "metadata": {}, "outputs": [], "source": [ - "X = dataset.drop_columns(columns=['Primary Type', 'FBI Code'])\n", - "y = dataset.keep_columns(columns=['Primary Type'], validate=True)\n", - "print('X and y are ready!')" + "dataset.take(5).to_pandas_dataframe()" ] }, { @@ -244,19 +239,18 @@ "outputs": [], "source": [ "automl_settings = {\n", - " \"iteration_timeout_minutes\" : 5,\n", - " \"iterations\" : 2,\n", - " \"primary_metric\" : 'AUC_weighted',\n", - " \"preprocess\" : True,\n", - " \"verbosity\" : logging.INFO\n", + " \"experiment_timeout_minutes\": 20,\n", + " \"max_concurrent_iterations\": 4,\n", + " \"primary_metric\" : 'AUC_weighted'\n", "}\n", - "automl_config = AutoMLConfig(task = 'classification',\n", - " debug_log = 'automl_errors.log',\n", + "automl_config = AutoMLConfig(compute_target=compute_target,\n", + " task = \"classification\",\n", + " training_data=dataset,\n", + " label_column_name=\"Primary Type\", \n", " path = project_folder,\n", - " compute_target=compute_target,\n", - " run_configuration=conda_run_config,\n", - " X = X,\n", - " y = y,\n", + " enable_early_stopping= True,\n", + " featurization= 'auto',\n", + " debug_log = \"automl_errors.log\",\n", " **automl_settings\n", " )" ] @@ -265,6 +259,8 @@ "cell_type": "markdown", "metadata": {}, "source": [ + "#### Create Pipeline and AutoMLStep\n", + "\n", "You can define outputs for the AutoMLStep using TrainingOutput." ] }, @@ -300,7 +296,11 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "tags": [ + "automlstep-remarks-sample1" + ] + }, "outputs": [], "source": [ "automl_step = AutoMLStep(\n", @@ -313,7 +313,11 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "tags": [ + "automlstep-remarks-sample2" + ] + }, "outputs": [], "source": [ "from azureml.pipeline.core import Pipeline\n", @@ -378,8 +382,8 @@ "outputs": [], "source": [ "import json\n", - "with open(metrics_output._path_on_datastore) as f: \n", - " metrics_output_result = f.read()\n", + "with open(metrics_output._path_on_datastore) as f:\n", + " metrics_output_result = f.read()\n", " \n", "deserialized_metrics_output = json.loads(metrics_output_result)\n", "df = pd.DataFrame(deserialized_metrics_output)\n", @@ -399,6 +403,7 @@ "metadata": {}, "outputs": [], "source": [ + "# Retrieve best model from Pipeline Run\n", "best_model_output = pipeline_run.get_pipeline_output(best_model_output_name)\n", "num_file_downloaded = best_model_output.download('.', show_progress=True)" ] @@ -416,6 +421,15 @@ "best_model" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "best_model.steps" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -431,11 +445,11 @@ "metadata": {}, "outputs": [], "source": [ - "dataset = Dataset.Tabular.from_delimited_files(path='https://dprepdata.blob.core.windows.net/demo/crime0-test.csv')\n", + "dataset_test = Dataset.Tabular.from_delimited_files(path='https://dprepdata.blob.core.windows.net/demo/crime0-test.csv')\n", "df_test = dataset_test.to_pandas_dataframe()\n", - "df_test = df_test[pd.notnull(df['Primary Type'])]\n", + "df_test = df_test[pd.notnull(df_test['Primary Type'])]\n", "\n", - "y_test = df_test[['Primary Type']]\n", + "y_test = df_test['Primary Type']\n", "X_test = df_test.drop(['Primary Type', 'FBI Code'], axis=1)" ] }, @@ -454,15 +468,19 @@ "metadata": {}, "outputs": [], "source": [ - "from pandas_ml import ConfusionMatrix\n", - "\n", + "from sklearn.metrics import confusion_matrix\n", "ypred = best_model.predict(X_test)\n", - "\n", - "cm = ConfusionMatrix(y_test['Primary Type'], ypred)\n", - "\n", - "print(cm)\n", - "\n", - "cm.plot()" + "cm = confusion_matrix(y_test, ypred)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Visualize the confusion matrix\n", + "pd.DataFrame(cm).style.background_gradient(cmap='Blues', low=0, high=0.9)" ] } ], diff --git a/how-to-use-azureml/machine-learning-pipelines/nyc-taxi-data-regression-model-building/nyc-taxi-data-regression-model-building.ipynb b/how-to-use-azureml/machine-learning-pipelines/nyc-taxi-data-regression-model-building/nyc-taxi-data-regression-model-building.ipynb index 727de886..aace1bf8 100644 --- a/how-to-use-azureml/machine-learning-pipelines/nyc-taxi-data-regression-model-building/nyc-taxi-data-regression-model-building.ipynb +++ b/how-to-use-azureml/machine-learning-pipelines/nyc-taxi-data-regression-model-building/nyc-taxi-data-regression-model-building.ipynb @@ -16,16 +16,12 @@ "\n", "You can combine the two part tutorial into one using AzureML Pipelines as Pipelines provide a way to stitch together various steps involved (like data preparation and training in this case) in a machine learning workflow.\n", "\n", - "In this notebook, you learn how to prepare data for regression modeling by using the [Azure Machine Learning Data Prep SDK](https://aka.ms/data-prep-sdk) for Python. You run various transformations to filter and combine two different NYC taxi data sets. Once you prepare the NYC taxi data for regression modeling, then you will use [AutoMLStep](https://docs.microsoft.com/en-us/python/api/azureml-train-automl/azureml.train.automl.automlstep?view=azure-ml-py) available with [Azure Machine Learning Pipelines](https://aka.ms/aml-pipelines) to define your machine learning goals and constraints as well as to launch the automated machine learning process. The automated machine learning technique iterates over many combinations of algorithms and hyperparameters until it finds the best model based on your criterion.\n", + "In this notebook, you learn how to prepare data for regression modeling by using open source library [pandas](https://pandas.pydata.org/). You run various transformations to filter and combine two different NYC taxi datasets. Once you prepare the NYC taxi data for regression modeling, then you will use [AutoMLStep](https://docs.microsoft.com/python/api/azureml-train-automl-runtime/azureml.train.automl.runtime.automl_step.automlstep?view=azure-ml-py) available with [Azure Machine Learning Pipelines](https://aka.ms/aml-pipelines) to define your machine learning goals and constraints as well as to launch the automated machine learning process. The automated machine learning technique iterates over many combinations of algorithms and hyperparameters until it finds the best model based on your criterion.\n", "\n", "After you complete building the model, you can predict the cost of a taxi trip by training a model on data features. These features include the pickup day and time, the number of passengers, and the pickup location.\n", "\n", "## Prerequisite\n", - "If you are using an Azure Machine Learning Notebook VM, you are all set. Otherwise, make sure you go through the configuration Notebook located at https://github.com/Azure/MachineLearningNotebooks first if you haven't. This sets you up with a working config file that has information on your workspace, subscription id, etc.\n", - "\n", - "We will run various transformations to filter and combine two different NYC taxi data sets. We will use DataPrep SDK for this preparing data. \n", - "\n", - "Perform `pip install azureml-dataprep` if you have't already done so." + "If you are using an Azure Machine Learning Notebook VM, you are all set. Otherwise, make sure you go through the configuration Notebook located at https://github.com/Azure/MachineLearningNotebooks first if you haven't. This sets you up with a working config file that has information on your workspace, subscription id, etc." ] }, { @@ -108,7 +104,6 @@ "metadata": {}, "outputs": [], "source": [ - "import azureml.dataprep as dprep\n", "from IPython.display import display\n", "\n", "display(green_df_raw.head(5))\n", @@ -144,8 +139,8 @@ "if not os.path.exists(yelloDir):\n", " os.mkdir(yelloDir)\n", " \n", - "greenTaxiData = greenDir + \"/part-00000\"\n", - "yellowTaxiData = yelloDir + \"/part-00000\"\n", + "greenTaxiData = greenDir + \"/unprepared.parquet\"\n", + "yellowTaxiData = yelloDir + \"/unprepared.parquet\"\n", "\n", "green_df_raw.to_csv(greenTaxiData, index=False)\n", "yellow_df_raw.to_csv(yellowTaxiData, index=False)\n", @@ -169,17 +164,54 @@ "\n", "default_store.upload_files([greenTaxiData], \n", " target_path = 'green', \n", - " overwrite = False, \n", + " overwrite = True, \n", " show_progress = True)\n", "\n", "default_store.upload_files([yellowTaxiData], \n", " target_path = 'yellow', \n", - " overwrite = False, \n", + " overwrite = True, \n", " show_progress = True)\n", "\n", "print(\"Upload calls completed.\")" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Create and register datasets\n", + "\n", + "By creating a dataset, you create a reference to the data source location. If you applied any subsetting transformations to the dataset, they will be stored in the dataset as well. You can learn more about the what subsetting capabilities are supported by referring to [our documentation](https://docs.microsoft.com/en-us/python/api/azureml-core/azureml.data.tabular_dataset.tabulardataset?view=azure-ml-py#remarks). The data remains in its existing location, so no extra storage cost is incurred." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from azureml.core import Dataset\n", + "green_taxi_data = Dataset.Tabular.from_delimited_files(default_store.path('green/unprepared.parquet'))\n", + "yellow_taxi_data = Dataset.Tabular.from_delimited_files(default_store.path('yellow/unprepared.parquet'))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Register the taxi datasets with the workspace so that you can reuse them in other experiments or share with your colleagues who have access to your workspace." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "green_taxi_data = green_taxi_data.register(ws, 'green_taxi_data')\n", + "yellow_taxi_data = yellow_taxi_data.register(ws, 'yellow_taxi_data')" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -194,20 +226,22 @@ "metadata": {}, "outputs": [], "source": [ - "from azureml.core.compute import AmlCompute\n", - "from azureml.core.compute import ComputeTarget\n", + "from azureml.core.compute import ComputeTarget, AmlCompute\n", + "from azureml.core.compute_target import ComputeTargetException\n", "\n", - "aml_compute = ws.get_default_compute_target(\"CPU\")\n", + "# Choose a name for your CPU cluster\n", + "amlcompute_cluster_name = \"cpu-cluster\"\n", "\n", - "if aml_compute is None:\n", - " amlcompute_cluster_name = \"cpu-cluster\"\n", - " provisioning_config = AmlCompute.provisioning_configuration(vm_size = \"STANDARD_D2_V2\",\n", - " max_nodes = 4)\n", + "# Verify that cluster does not exist already\n", + "try:\n", + " aml_compute = ComputeTarget(workspace=ws, name=amlcompute_cluster_name)\n", + " print('Found existing cluster, use it.')\n", + "except ComputeTargetException:\n", + " compute_config = AmlCompute.provisioning_configuration(vm_size='STANDARD_D2_V2',\n", + " max_nodes=4)\n", + " aml_compute = ComputeTarget.create(ws, amlcompute_cluster_name, compute_config)\n", "\n", - " aml_compute = ComputeTarget.create(ws, amlcompute_cluster_name, provisioning_config)\n", - " aml_compute.wait_for_completion(show_output = True, min_node_count = None, timeout_in_minutes = 20)\n", - "\n", - "aml_compute" + "aml_compute.wait_for_completion(show_output=True)" ] }, { @@ -215,7 +249,7 @@ "metadata": {}, "source": [ "#### Define RunConfig for the compute\n", - "We need `azureml-dataprep` SDK for all the steps below. We will also use `pandas`, `scikit-learn` and `automl` for the training step. Defining the `runconfig` for that." + "We will also use `pandas`, `scikit-learn` and `automl`, `pyarrow` for the pipeline steps. Defining the `runconfig` for that." ] }, { @@ -242,13 +276,10 @@ "# Use conda_dependencies.yml to create a conda environment in the Docker image for execution\n", "aml_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", - "aml_run_config.auto_prepare_environment = True\n", - "\n", "# Specify CondaDependencies obj, add necessary packages\n", "aml_run_config.environment.python.conda_dependencies = CondaDependencies.create(\n", " conda_packages=['pandas','scikit-learn'], \n", - " pip_packages=['azureml-sdk', 'azureml-dataprep', 'azureml-train-automl'], \n", + " pip_packages=['azureml-sdk[automl,explain]', 'pyarrow'], \n", " pin_sdk_version=False)\n", "\n", "print (\"Run configuration created.\")" @@ -259,7 +290,7 @@ "metadata": {}, "source": [ "### Prepare data\n", - "Now we will prepare for regression modeling by using the `Azure Machine Learning Data Prep SDK for Python`. We run various transformations to filter and combine two different NYC taxi data sets.\n", + "Now we will prepare for regression modeling by using `pandas`. We run various transformations to filter and combine two different NYC taxi datasets.\n", "\n", "We achieve this by creating a separate step for each transformation as this allows us to reuse the steps and saves us from running all over again in case of any change. We will keep data preparation scripts in one subfolder and training scripts in another.\n", "\n", @@ -270,7 +301,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "#### Define Useful Colums\n", + "#### Define Useful Columns\n", "Here we are defining a set of \"useful\" columns for both Green and Yellow taxi data." ] }, @@ -304,18 +335,12 @@ "metadata": {}, "outputs": [], "source": [ - "from azureml.data.data_reference import DataReference \n", "from azureml.pipeline.core import PipelineData\n", "from azureml.pipeline.steps import PythonScriptStep\n", "\n", "# python scripts folder\n", "prepare_data_folder = './scripts/prepdata'\n", "\n", - "blob_green_data = DataReference(\n", - " datastore=default_store,\n", - " data_reference_name=\"green_taxi_data\",\n", - " path_on_datastore=\"green/part-00000\")\n", - "\n", "# rename columns as per Azure Machine Learning NYC Taxi tutorial\n", "green_columns = str({ \n", " \"vendorID\": \"vendor\",\n", @@ -332,7 +357,7 @@ "}).replace(\",\", \";\")\n", "\n", "# Define output after cleansing step\n", - "cleansed_green_data = PipelineData(\"green_taxi_data\", datastore=default_store)\n", + "cleansed_green_data = PipelineData(\"cleansed_green_data\", datastore=default_store).as_dataset()\n", "\n", "print('Cleanse script is in {}.'.format(os.path.realpath(prepare_data_folder)))\n", "\n", @@ -341,11 +366,10 @@ "cleansingStepGreen = PythonScriptStep(\n", " name=\"Cleanse Green Taxi Data\",\n", " script_name=\"cleanse.py\", \n", - " arguments=[\"--input_cleanse\", blob_green_data, \n", - " \"--useful_columns\", useful_columns,\n", + " arguments=[\"--useful_columns\", useful_columns,\n", " \"--columns\", green_columns,\n", " \"--output_cleanse\", cleansed_green_data],\n", - " inputs=[blob_green_data],\n", + " inputs=[green_taxi_data.as_named_input('raw_data')],\n", " outputs=[cleansed_green_data],\n", " compute_target=aml_compute,\n", " runconfig=aml_run_config,\n", @@ -369,11 +393,6 @@ "metadata": {}, "outputs": [], "source": [ - "blob_yellow_data = DataReference(\n", - " datastore=default_store,\n", - " data_reference_name=\"yellow_taxi_data\",\n", - " path_on_datastore=\"yellow/part-00000\")\n", - "\n", "yellow_columns = str({\n", " \"vendorID\": \"vendor\",\n", " \"tpepPickupDateTime\": \"pickup_datetime\",\n", @@ -389,7 +408,7 @@ "}).replace(\",\", \";\")\n", "\n", "# Define output after cleansing step\n", - "cleansed_yellow_data = PipelineData(\"yellow_taxi_data\", datastore=default_store)\n", + "cleansed_yellow_data = PipelineData(\"cleansed_yellow_data\", datastore=default_store).as_dataset()\n", "\n", "print('Cleanse script is in {}.'.format(os.path.realpath(prepare_data_folder)))\n", "\n", @@ -398,11 +417,10 @@ "cleansingStepYellow = PythonScriptStep(\n", " name=\"Cleanse Yellow Taxi Data\",\n", " script_name=\"cleanse.py\", \n", - " arguments=[\"--input_cleanse\", blob_yellow_data, \n", - " \"--useful_columns\", useful_columns,\n", + " arguments=[\"--useful_columns\", useful_columns,\n", " \"--columns\", yellow_columns,\n", " \"--output_cleanse\", cleansed_yellow_data],\n", - " inputs=[blob_yellow_data],\n", + " inputs=[yellow_taxi_data.as_named_input('raw_data')],\n", " outputs=[cleansed_yellow_data],\n", " compute_target=aml_compute,\n", " runconfig=aml_run_config,\n", @@ -428,7 +446,7 @@ "outputs": [], "source": [ "# Define output after merging step\n", - "merged_data = PipelineData(\"merged_data\", datastore=default_store)\n", + "merged_data = PipelineData(\"merged_data\", datastore=default_store).as_dataset()\n", "\n", "print('Merge script is in {}.'.format(os.path.realpath(prepare_data_folder)))\n", "\n", @@ -437,10 +455,9 @@ "mergingStep = PythonScriptStep(\n", " name=\"Merge Taxi Data\",\n", " script_name=\"merge.py\", \n", - " arguments=[\"--input_green_merge\", cleansed_green_data, \n", - " \"--input_yellow_merge\", cleansed_yellow_data,\n", - " \"--output_merge\", merged_data],\n", - " inputs=[cleansed_green_data, cleansed_yellow_data],\n", + " arguments=[\"--output_merge\", merged_data],\n", + " inputs=[cleansed_green_data.parse_parquet_files(file_extension=None),\n", + " cleansed_yellow_data.parse_parquet_files(file_extension=None)],\n", " outputs=[merged_data],\n", " compute_target=aml_compute,\n", " runconfig=aml_run_config,\n", @@ -466,7 +483,7 @@ "outputs": [], "source": [ "# Define output after merging step\n", - "filtered_data = PipelineData(\"filtered_data\", datastore=default_store)\n", + "filtered_data = PipelineData(\"filtered_data\", datastore=default_store).as_dataset()\n", "\n", "print('Filter script is in {}.'.format(os.path.realpath(prepare_data_folder)))\n", "\n", @@ -475,9 +492,8 @@ "filterStep = PythonScriptStep(\n", " name=\"Filter Taxi Data\",\n", " script_name=\"filter.py\", \n", - " arguments=[\"--input_filter\", merged_data, \n", - " \"--output_filter\", filtered_data],\n", - " inputs=[merged_data],\n", + " arguments=[\"--output_filter\", filtered_data],\n", + " inputs=[merged_data.parse_parquet_files(file_extension=None)],\n", " outputs=[filtered_data],\n", " compute_target=aml_compute,\n", " runconfig = aml_run_config,\n", @@ -503,7 +519,7 @@ "outputs": [], "source": [ "# Define output after normalize step\n", - "normalized_data = PipelineData(\"normalized_data\", datastore=default_store)\n", + "normalized_data = PipelineData(\"normalized_data\", datastore=default_store).as_dataset()\n", "\n", "print('Normalize script is in {}.'.format(os.path.realpath(prepare_data_folder)))\n", "\n", @@ -512,9 +528,8 @@ "normalizeStep = PythonScriptStep(\n", " name=\"Normalize Taxi Data\",\n", " script_name=\"normalize.py\", \n", - " arguments=[\"--input_normalize\", filtered_data, \n", - " \"--output_normalize\", normalized_data],\n", - " inputs=[filtered_data],\n", + " arguments=[\"--output_normalize\", normalized_data],\n", + " inputs=[filtered_data.parse_parquet_files(file_extension=None)],\n", " outputs=[normalized_data],\n", " compute_target=aml_compute,\n", " runconfig = aml_run_config,\n", @@ -544,8 +559,8 @@ "metadata": {}, "outputs": [], "source": [ - "# Define output after transforme step\n", - "transformed_data = PipelineData(\"transformed_data\", datastore=default_store)\n", + "# Define output after transform step\n", + "transformed_data = PipelineData(\"transformed_data\", datastore=default_store).as_dataset()\n", "\n", "print('Transform script is in {}.'.format(os.path.realpath(prepare_data_folder)))\n", "\n", @@ -554,9 +569,8 @@ "transformStep = PythonScriptStep(\n", " name=\"Transform Taxi Data\",\n", " script_name=\"transform.py\", \n", - " arguments=[\"--input_transform\", normalized_data,\n", - " \"--output_transform\", transformed_data],\n", - " inputs=[normalized_data],\n", + " arguments=[\"--output_transform\", transformed_data],\n", + " inputs=[normalized_data.parse_parquet_files(file_extension=None)],\n", " outputs=[transformed_data],\n", " compute_target=aml_compute,\n", " runconfig = aml_run_config,\n", @@ -571,8 +585,8 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Extract features\n", - "Add the following columns to be features for our model creation. The prediction value will be *cost*." + "### Split the data into train and test sets\n", + "This function segregates the data into dataset for model training and dataset for testing." ] }, { @@ -581,92 +595,11 @@ "metadata": {}, "outputs": [], "source": [ - "feature_columns = str(['pickup_weekday','pickup_hour', 'distance','passengers', 'vendor']).replace(\",\", \";\")\n", - "\n", "train_model_folder = './scripts/trainmodel'\n", "\n", - "print('Extract script is in {}.'.format(os.path.realpath(train_model_folder)))\n", - "\n", - "# features data after transform step\n", - "features_data = PipelineData(\"features_data\", datastore=default_store)\n", - "\n", - "# featurization step creation\n", - "# See the featurization.py for details about input and output\n", - "featurizationStep = PythonScriptStep(\n", - " name=\"Extract Features\",\n", - " script_name=\"featurization.py\", \n", - " arguments=[\"--input_featurization\", transformed_data, \n", - " \"--useful_columns\", feature_columns,\n", - " \"--output_featurization\", features_data],\n", - " inputs=[transformed_data],\n", - " outputs=[features_data],\n", - " compute_target=aml_compute,\n", - " runconfig = aml_run_config,\n", - " source_directory=train_model_folder,\n", - " allow_reuse=True\n", - ")\n", - "\n", - "print(\"featurizationStep created.\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Extract label" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "label_columns = str(['cost']).replace(\",\", \";\")\n", - "\n", - "# label data after transform step\n", - "label_data = PipelineData(\"label_data\", datastore=default_store)\n", - "\n", - "print('Extract script is in {}.'.format(os.path.realpath(train_model_folder)))\n", - "\n", - "# label step creation\n", - "# See the featurization.py for details about input and output\n", - "labelStep = PythonScriptStep(\n", - " name=\"Extract Labels\",\n", - " script_name=\"featurization.py\", \n", - " arguments=[\"--input_featurization\", transformed_data, \n", - " \"--useful_columns\", label_columns,\n", - " \"--output_featurization\", label_data],\n", - " inputs=[transformed_data],\n", - " outputs=[label_data],\n", - " compute_target=aml_compute,\n", - " runconfig = aml_run_config,\n", - " source_directory=train_model_folder,\n", - " allow_reuse=True\n", - ")\n", - "\n", - "print(\"labelStep created.\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Split the data into train and test sets\n", - "This function segregates the data into the **x**, features, dataset for model training and **y**, values to predict, dataset for testing." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ "# train and test splits output\n", - "output_split_train_x = PipelineData(\"output_split_train_x\", datastore=default_store)\n", - "output_split_train_y = PipelineData(\"output_split_train_y\", datastore=default_store)\n", - "output_split_test_x = PipelineData(\"output_split_test_x\", datastore=default_store)\n", - "output_split_test_y = PipelineData(\"output_split_test_y\", datastore=default_store)\n", + "output_split_train = PipelineData(\"output_split_train\", datastore=default_store).as_dataset()\n", + "output_split_test = PipelineData(\"output_split_test\", datastore=default_store).as_dataset()\n", "\n", "print('Data spilt script is in {}.'.format(os.path.realpath(train_model_folder)))\n", "\n", @@ -675,14 +608,10 @@ "testTrainSplitStep = PythonScriptStep(\n", " name=\"Train Test Data Split\",\n", " script_name=\"train_test_split.py\", \n", - " arguments=[\"--input_split_features\", features_data, \n", - " \"--input_split_labels\", label_data,\n", - " \"--output_split_train_x\", output_split_train_x,\n", - " \"--output_split_train_y\", output_split_train_y,\n", - " \"--output_split_test_x\", output_split_test_x,\n", - " \"--output_split_test_y\", output_split_test_y],\n", - " inputs=[features_data, label_data],\n", - " outputs=[output_split_train_x, output_split_train_y, output_split_test_x, output_split_test_y],\n", + " arguments=[\"--output_split_train\", output_split_train,\n", + " \"--output_split_test\", output_split_test],\n", + " inputs=[transformed_data.parse_parquet_files(file_extension=None)],\n", + " outputs=[output_split_train, output_split_test],\n", " compute_target=aml_compute,\n", " runconfig = aml_run_config,\n", " source_directory=train_model_folder,\n", @@ -697,7 +626,7 @@ "metadata": {}, "source": [ "## Use automated machine learning to build regression model\n", - "Now we will use **automated machine learning** to build the regression model. We will use [AutoMLStep](https://docs.microsoft.com/en-us/python/api/azureml-train-automl/azureml.train.automl.automlstep?view=azure-ml-py) in AML Pipelines for this part. These functions use various features from the data set and allow an automated model to build relationships between the features and the price of a taxi trip." + "Now we will use **automated machine learning** to build the regression model. We will use [AutoMLStep](https://docs.microsoft.com/python/api/azureml-train-automl-runtime/azureml.train.automl.runtime.automl_step.automlstep?view=azure-ml-py) in AML Pipelines for this part. Perform `pip install azureml-sdk[automl]`to get the automated machine learning package. These functions use various features from the data set and allow an automated model to build relationships between the features and the price of a taxi trip." ] }, { @@ -727,52 +656,13 @@ "print(\"Experiment created\")" ] }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Create get_data script\n", - "\n", - "A script with `get_data()` function is necessary to fetch training features(X) and labels(Y) on remote compute, from input data. Here we use mounted path of `train_test_split` step to get the x and y train values. They are added as environment variable on compute machine by default\n", - "\n", - "Note: Every DataReference are added as environment variable on compute machine since the defualt mode is mount" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "print('get_data.py will be written to {}.'.format(os.path.realpath(train_model_folder)))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%%writefile $train_model_folder/get_data.py\n", - "import os\n", - "import pandas as pd\n", - "\n", - "def get_data():\n", - " print(\"In get_data\")\n", - " print(os.environ['AZUREML_DATAREFERENCE_output_split_train_x'])\n", - " X_train = pd.read_csv(os.environ['AZUREML_DATAREFERENCE_output_split_train_x'] + \"/part-00000\", header=0)\n", - " y_train = pd.read_csv(os.environ['AZUREML_DATAREFERENCE_output_split_train_y'] + \"/part-00000\", header=0)\n", - " \n", - " return { \"X\" : X_train.values, \"y\" : y_train.values.flatten() }" - ] - }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Define settings for autogeneration and tuning\n", "\n", - "Here we define the experiment parameter and model settings for autogeneration and tuning. We can specify automl_settings as **kwargs as well. Also note that we have to use a get_data() function for remote excutions. See get_data script for more details.\n", + "Here we define the experiment parameter and model settings for autogeneration and tuning. We can specify automl_settings as **kwargs as well.\n", "\n", "Use your defined training settings as a parameter to an `AutoMLConfig` object. Additionally, specify your training data and the type of model, which is `regression` in this case.\n", "\n", @@ -793,17 +683,20 @@ " \"iteration_timeout_minutes\" : 10,\n", " \"iterations\" : 2,\n", " \"primary_metric\" : 'spearman_correlation',\n", - " \"preprocess\" : True,\n", - " \"verbosity\" : logging.INFO,\n", " \"n_cross_validations\": 5\n", "}\n", "\n", + "train_X = output_split_train.parse_parquet_files(file_extension=None).keep_columns(['pickup_weekday','pickup_hour', 'distance','passengers', 'vendor'])\n", + "train_y = output_split_train.parse_parquet_files(file_extension=None).keep_columns('cost')\n", + "\n", "automl_config = AutoMLConfig(task = 'regression',\n", " debug_log = 'automated_ml_errors.log',\n", " path = train_model_folder,\n", - " compute_target=aml_compute,\n", - " run_configuration=aml_run_config,\n", - " data_script = train_model_folder + \"/get_data.py\",\n", + " compute_target = aml_compute,\n", + " run_configuration = aml_run_config,\n", + " featurization = 'auto',\n", + " X = train_X,\n", + " y = train_y,\n", " **automl_settings)\n", " \n", "print(\"AutoML config created.\")" @@ -822,15 +715,12 @@ "metadata": {}, "outputs": [], "source": [ - "from azureml.train.automl.runtime import AutoMLStep\n", - "\n", - "trainWithAutomlStep = AutoMLStep(\n", - " name='AutoML_Regression',\n", - " automl_config=automl_config,\n", - " inputs=[output_split_train_x, output_split_train_y],\n", - " allow_reuse=True,\n", - " hash_paths=[os.path.realpath(train_model_folder)])\n", + "from azureml.pipeline.steps import AutoMLStep\n", "\n", + "trainWithAutomlStep = AutoMLStep(name='AutoML_Regression',\n", + " automl_config=automl_config,\n", + " passthru_automl_config=False,\n", + " allow_reuse=True)\n", "print(\"trainWithAutomlStep created.\")" ] }, @@ -892,12 +782,11 @@ " return path\n", "\n", "def fetch_df(step, output_name):\n", - " output_data = step.get_output_data(output_name)\n", - " \n", + " output_data = step.get_output_data(output_name) \n", " download_path = './outputs/' + output_name\n", - " output_data.download(download_path)\n", - " df_path = get_download_path(download_path, output_name) + '/part-00000'\n", - " return dprep.auto_read_file(path=df_path)" + " output_data.download(download_path, overwrite=True)\n", + " df_path = get_download_path(download_path, output_name) + '/processed.parquet'\n", + " return pd.read_parquet(df_path)" ] }, { @@ -939,7 +828,7 @@ "merge_step = pipeline_run.find_step_run(mergingStep.name)[0]\n", "combined_df = fetch_df(merge_step, merged_data.name)\n", "\n", - "display(combined_df.get_profile())" + "display(combined_df.describe())" ] }, { @@ -958,7 +847,7 @@ "filter_step = pipeline_run.find_step_run(filterStep.name)[0]\n", "filtered_df = fetch_df(filter_step, filtered_data.name)\n", "\n", - "display(filtered_df.get_profile())" + "display(filtered_df.describe())" ] }, { @@ -996,7 +885,7 @@ "transform_step = pipeline_run.find_step_run(transformStep.name)[0]\n", "transformed_df = fetch_df(transform_step, transformed_data.name)\n", "\n", - "display(transformed_df.get_profile())\n", + "display(transformed_df.describe())\n", "display(transformed_df.head(5))" ] }, @@ -1014,16 +903,10 @@ "outputs": [], "source": [ "split_step = pipeline_run.find_step_run(testTrainSplitStep.name)[0]\n", - "train_split_x = fetch_df(split_step, output_split_train_x.name)\n", - "train_split_y = fetch_df(split_step, output_split_train_y.name)\n", + "train_split = fetch_df(split_step, output_split_train.name)\n", "\n", - "display_x_train = train_split_x.keep_columns(columns=[\"vendor\", \"pickup_weekday\", \"pickup_hour\", \"passengers\", \"distance\"])\n", - "display_y_train = train_split_y.rename_columns(column_pairs={\"Column1\": \"cost\"})\n", - "\n", - "display(display_x_train.get_profile())\n", - "display(display_x_train.head(5))\n", - "display(display_y_train.get_profile())\n", - "display(display_y_train.head(5))" + "display(train_split.describe())\n", + "display(train_split.head(5))" ] }, { @@ -1125,14 +1008,11 @@ "source": [ "# split_step = pipeline_run.find_step_run(testTrainSplitStep.name)[0]\n", "\n", - "# x_test = fetch_df(split_step, output_split_test_x.name)\n", - "# y_test = fetch_df(split_step, output_split_test_y.name)\n", + "# x_test = fetch_df(split_step, output_split_test.name)[['distance','passengers', 'vendor','pickup_weekday','pickup_hour']]\n", + "# y_test = fetch_df(split_step, output_split_test.name)[['cost']]\n", "\n", - "# display(x_test.keep_columns(columns=[\"vendor\", \"pickup_weekday\", \"pickup_hour\", \"passengers\", \"distance\"]).head(5))\n", - "# display(y_test.rename_columns(column_pairs={\"Column1\": \"cost\"}).head(5))\n", - "\n", - "# x_test = x_test.to_pandas_dataframe()\n", - "# y_test = y_test.to_pandas_dataframe()" + "# display(x_test.head(5))\n", + "# display(y_test.head(5))" ] }, { @@ -1150,9 +1030,9 @@ "metadata": {}, "outputs": [], "source": [ - "# y_predict = fitted_model.predict(x_test.values)\n", + "# y_predict = fitted_model.predict(x_test)\n", "\n", - "# y_actual = y_test.iloc[:,0].values.tolist()\n", + "# y_actual = y_test.values.tolist()\n", "\n", "# display(pd.DataFrame({'Actual':y_actual, 'Predicted':y_predict}).head(5))" ] @@ -1168,7 +1048,7 @@ "# fig = plt.figure(figsize=(14, 10))\n", "# ax1 = fig.add_subplot(111)\n", "\n", - "# distance_vals = [x[4] for x in x_test.values]\n", + "# distance_vals = [x[0] for x in x_test.values]\n", "\n", "# ax1.scatter(distance_vals[:100], y_predict[:100], s=18, c='b', marker=\"s\", label='Predicted')\n", "# ax1.scatter(distance_vals[:100], y_actual[:100], s=18, c='r', marker=\"o\", label='Actual')\n", @@ -1204,7 +1084,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.7" + "version": "3.6.9" } }, "nbformat": 4, diff --git a/how-to-use-azureml/machine-learning-pipelines/nyc-taxi-data-regression-model-building/nyc-taxi-data-regression-model-building.yml b/how-to-use-azureml/machine-learning-pipelines/nyc-taxi-data-regression-model-building/nyc-taxi-data-regression-model-building.yml index dcdee696..12b58a21 100644 --- a/how-to-use-azureml/machine-learning-pipelines/nyc-taxi-data-regression-model-building/nyc-taxi-data-regression-model-building.yml +++ b/how-to-use-azureml/machine-learning-pipelines/nyc-taxi-data-regression-model-building/nyc-taxi-data-regression-model-building.yml @@ -4,6 +4,7 @@ dependencies: - azureml-sdk - azureml-widgets - azureml-opendatasets - - azureml-dataprep - azureml-train-automl - matplotlib + - pandas + - pyarrow diff --git a/how-to-use-azureml/machine-learning-pipelines/nyc-taxi-data-regression-model-building/scripts/prepdata/cleanse.py b/how-to-use-azureml/machine-learning-pipelines/nyc-taxi-data-regression-model-building/scripts/prepdata/cleanse.py index 0b8c4143..bae27e82 100644 --- a/how-to-use-azureml/machine-learning-pipelines/nyc-taxi-data-regression-model-building/scripts/prepdata/cleanse.py +++ b/how-to-use-azureml/machine-learning-pipelines/nyc-taxi-data-regression-model-building/scripts/prepdata/cleanse.py @@ -3,15 +3,14 @@ import argparse import os -import pandas as pd -import azureml.dataprep as dprep +from azureml.core import Run def get_dict(dict_str): pairs = dict_str.strip("{}").split("\;") new_dict = {} for pair in pairs: - key, value = pair.strip('\\').split(":") + key, value = pair.strip().split(":") new_dict[key.strip().strip("'")] = value.strip().strip("'") return new_dict @@ -19,40 +18,37 @@ def get_dict(dict_str): print("Cleans the input data") +# Get the input green_taxi_data. To learn more about how to access dataset in your script, please +# see https://docs.microsoft.com/en-us/azure/machine-learning/how-to-train-with-datasets. +run = Run.get_context() +raw_data = run.input_datasets["raw_data"] + + parser = argparse.ArgumentParser("cleanse") -parser.add_argument("--input_cleanse", type=str, help="raw taxi data") parser.add_argument("--output_cleanse", type=str, help="cleaned taxi data directory") parser.add_argument("--useful_columns", type=str, help="useful columns to keep") parser.add_argument("--columns", type=str, help="rename column pattern") args = parser.parse_args() -print("Argument 1(input taxi data path): %s" % args.input_cleanse) -print("Argument 2(columns to keep): %s" % str(args.useful_columns.strip("[]").split("\;"))) -print("Argument 3(columns renaming mapping): %s" % str(args.columns.strip("{}").split("\;"))) -print("Argument 4(output cleansed taxi data path): %s" % args.output_cleanse) +print("Argument 1(columns to keep): %s" % str(args.useful_columns.strip("[]").split("\;"))) +print("Argument 2(columns renaming mapping): %s" % str(args.columns.strip("{}").split("\;"))) +print("Argument 3(output cleansed taxi data path): %s" % args.output_cleanse) -raw_df = dprep.read_csv(path=args.input_cleanse, header=dprep.PromoteHeadersMode.GROUPED) - -# These functions ensure that null data is removed from the data set, +# These functions ensure that null data is removed from the dataset, # which will help increase machine learning model accuracy. -# Visit https://docs.microsoft.com/en-us/azure/machine-learning/service/tutorial-data-prep -# for more details useful_columns = [s.strip().strip("'") for s in args.useful_columns.strip("[]").split("\;")] columns = get_dict(args.columns) -all_columns = dprep.ColumnSelector(term=".*", use_regex=True) -drop_if_all_null = [all_columns, dprep.ColumnRelationship(dprep.ColumnRelationship.ALL)] +new_df = (raw_data.to_pandas_dataframe() + .dropna(how='all') + .rename(columns=columns))[useful_columns] -new_df = (raw_df - .replace_na(columns=all_columns) - .drop_nulls(*drop_if_all_null) - .rename_columns(column_pairs=columns) - .keep_columns(columns=useful_columns)) +new_df.reset_index(inplace=True, drop=True) if not (args.output_cleanse is None): os.makedirs(args.output_cleanse, exist_ok=True) print("%s created" % args.output_cleanse) - write_df = new_df.write_to_csv(directory_path=dprep.LocalFileOutput(args.output_cleanse)) - write_df.run_local() + path = args.output_cleanse + "/processed.parquet" + write_df = new_df.to_parquet(path) diff --git a/how-to-use-azureml/machine-learning-pipelines/nyc-taxi-data-regression-model-building/scripts/prepdata/filter.py b/how-to-use-azureml/machine-learning-pipelines/nyc-taxi-data-regression-model-building/scripts/prepdata/filter.py index a7248185..a999c54e 100644 --- a/how-to-use-azureml/machine-learning-pipelines/nyc-taxi-data-regression-model-building/scripts/prepdata/filter.py +++ b/how-to-use-azureml/machine-learning-pipelines/nyc-taxi-data-regression-model-building/scripts/prepdata/filter.py @@ -1,55 +1,47 @@ import argparse import os -import azureml.dataprep as dprep +from azureml.core import Run print("Filters out coordinates for locations that are outside the city border.", "Chain the column filter commands within the filter() function", "and define the minimum and maximum bounds for each field.") +run = Run.get_context() + +# To learn more about how to access dataset in your script, please +# see https://docs.microsoft.com/en-us/azure/machine-learning/how-to-train-with-datasets. +merged_data = run.input_datasets["merged_data"] +combined_df = merged_data.to_pandas_dataframe() + parser = argparse.ArgumentParser("filter") -parser.add_argument("--input_filter", type=str, help="merged taxi data directory") parser.add_argument("--output_filter", type=str, help="filter out out of city locations") args = parser.parse_args() -print("Argument 1(input taxi data path): %s" % args.input_filter) -print("Argument 2(output filtered taxi data path): %s" % args.output_filter) - -combined_df = dprep.read_csv(args.input_filter + '/part-*') +print("Argument (output filtered taxi data path): %s" % args.output_filter) # These functions filter out coordinates for locations that are outside the city border. -# Visit https://docs.microsoft.com/en-us/azure/machine-learning/service/tutorial-data-prep for more details - -# Create a condensed view of the dataflow to just show the lat/long fields, -# which makes it easier to evaluate missing or out-of-scope coordinates -decimal_type = dprep.TypeConverter(data_type=dprep.FieldType.DECIMAL) -combined_df = combined_df.set_column_types(type_conversions={ - "pickup_longitude": decimal_type, - "pickup_latitude": decimal_type, - "dropoff_longitude": decimal_type, - "dropoff_latitude": decimal_type -}) # Filter out coordinates for locations that are outside the city border. # Chain the column filter commands within the filter() function # and define the minimum and maximum bounds for each field -latlong_filtered_df = (combined_df - .drop_nulls(columns=["pickup_longitude", - "pickup_latitude", - "dropoff_longitude", - "dropoff_latitude"], - column_relationship=dprep.ColumnRelationship(dprep.ColumnRelationship.ANY)) - .filter(dprep.f_and(dprep.col("pickup_longitude") <= -73.72, - dprep.col("pickup_longitude") >= -74.09, - dprep.col("pickup_latitude") <= 40.88, - dprep.col("pickup_latitude") >= 40.53, - dprep.col("dropoff_longitude") <= -73.72, - dprep.col("dropoff_longitude") >= -74.09, - dprep.col("dropoff_latitude") <= 40.88, - dprep.col("dropoff_latitude") >= 40.53))) + +combined_df = combined_df.astype({"pickup_longitude": 'float64', "pickup_latitude": 'float64', + "dropoff_longitude": 'float64', "dropoff_latitude": 'float64'}) + +latlong_filtered_df = combined_df[(combined_df.pickup_longitude <= -73.72) & + (combined_df.pickup_longitude >= -74.09) & + (combined_df.pickup_latitude <= 40.88) & + (combined_df.pickup_latitude >= 40.53) & + (combined_df.dropoff_longitude <= -73.72) & + (combined_df.dropoff_longitude >= -74.72) & + (combined_df.dropoff_latitude <= 40.88) & + (combined_df.dropoff_latitude >= 40.53)] + +latlong_filtered_df.reset_index(inplace=True, drop=True) if not (args.output_filter is None): os.makedirs(args.output_filter, exist_ok=True) print("%s created" % args.output_filter) - write_df = latlong_filtered_df.write_to_csv(directory_path=dprep.LocalFileOutput(args.output_filter)) - write_df.run_local() + path = args.output_filter + "/processed.parquet" + write_df = latlong_filtered_df.to_parquet(path) diff --git a/how-to-use-azureml/machine-learning-pipelines/nyc-taxi-data-regression-model-building/scripts/prepdata/merge.py b/how-to-use-azureml/machine-learning-pipelines/nyc-taxi-data-regression-model-building/scripts/prepdata/merge.py index 4764023a..bf3c8d93 100644 --- a/how-to-use-azureml/machine-learning-pipelines/nyc-taxi-data-regression-model-building/scripts/prepdata/merge.py +++ b/how-to-use-azureml/machine-learning-pipelines/nyc-taxi-data-regression-model-building/scripts/prepdata/merge.py @@ -1,29 +1,30 @@ - import argparse import os -import azureml.dataprep as dprep +from azureml.core import Run print("Merge Green and Yellow taxi data") +run = Run.get_context() + +# To learn more about how to access dataset in your script, please +# see https://docs.microsoft.com/en-us/azure/machine-learning/how-to-train-with-datasets. +cleansed_green_data = run.input_datasets["cleansed_green_data"] +cleansed_yellow_data = run.input_datasets["cleansed_yellow_data"] +green_df = cleansed_green_data.to_pandas_dataframe() +yellow_df = cleansed_yellow_data.to_pandas_dataframe() + parser = argparse.ArgumentParser("merge") -parser.add_argument("--input_green_merge", type=str, help="cleaned green taxi data directory") -parser.add_argument("--input_yellow_merge", type=str, help="cleaned yellow taxi data directory") parser.add_argument("--output_merge", type=str, help="green and yellow taxi data merged") args = parser.parse_args() - -print("Argument 1(input green taxi data path): %s" % args.input_green_merge) -print("Argument 2(input yellow taxi data path): %s" % args.input_yellow_merge) -print("Argument 3(output merge taxi data path): %s" % args.output_merge) - -green_df = dprep.read_csv(args.input_green_merge + '/part-*') -yellow_df = dprep.read_csv(args.input_yellow_merge + '/part-*') +print("Argument (output merge taxi data path): %s" % args.output_merge) # Appending yellow data to green data -combined_df = green_df.append_rows([yellow_df]) +combined_df = green_df.append(yellow_df, ignore_index=True) +combined_df.reset_index(inplace=True, drop=True) if not (args.output_merge is None): os.makedirs(args.output_merge, exist_ok=True) print("%s created" % args.output_merge) - write_df = combined_df.write_to_csv(directory_path=dprep.LocalFileOutput(args.output_merge)) - write_df.run_local() + path = args.output_merge + "/processed.parquet" + write_df = combined_df.to_parquet(path) diff --git a/how-to-use-azureml/machine-learning-pipelines/nyc-taxi-data-regression-model-building/scripts/prepdata/normalize.py b/how-to-use-azureml/machine-learning-pipelines/nyc-taxi-data-regression-model-building/scripts/prepdata/normalize.py index f7b384d1..589fd297 100644 --- a/how-to-use-azureml/machine-learning-pipelines/nyc-taxi-data-regression-model-building/scripts/prepdata/normalize.py +++ b/how-to-use-azureml/machine-learning-pipelines/nyc-taxi-data-regression-model-building/scripts/prepdata/normalize.py @@ -1,47 +1,48 @@ import argparse import os -import azureml.dataprep as dprep +import pandas as pd +from azureml.core import Run print("Replace undefined values to relavant values and rename columns to meaningful names") +run = Run.get_context() + +# To learn more about how to access dataset in your script, please +# see https://docs.microsoft.com/en-us/azure/machine-learning/how-to-train-with-datasets. +filtered_data = run.input_datasets['filtered_data'] +combined_converted_df = filtered_data.to_pandas_dataframe() + parser = argparse.ArgumentParser("normalize") -parser.add_argument("--input_normalize", type=str, help="combined and converted taxi data") parser.add_argument("--output_normalize", type=str, help="replaced undefined values and renamed columns") args = parser.parse_args() -print("Argument 1(input taxi data path): %s" % args.input_normalize) -print("Argument 2(output normalized taxi data path): %s" % args.output_normalize) - -combined_converted_df = dprep.read_csv(args.input_normalize + '/part-*') +print("Argument (output normalized taxi data path): %s" % args.output_normalize) # These functions replace undefined values and rename to use meaningful names. -# Visit https://docs.microsoft.com/en-us/azure/machine-learning/service/tutorial-data-prep for more details +replaced_stfor_vals_df = (combined_converted_df.replace({"store_forward": "0"}, {"store_forward": "N"}) + .fillna({"store_forward": "N"})) -replaced_stfor_vals_df = combined_converted_df.replace(columns="store_forward", - find="0", - replace_with="N").fill_nulls("store_forward", "N") +replaced_distance_vals_df = (replaced_stfor_vals_df.replace({"distance": ".00"}, {"distance": 0}) + .fillna({"distance": 0})) -replaced_distance_vals_df = replaced_stfor_vals_df.replace(columns="distance", - find=".00", - replace_with=0).fill_nulls("distance", 0) +normalized_df = replaced_distance_vals_df.astype({"distance": 'float64'}) -replaced_distance_vals_df = replaced_distance_vals_df.to_number(["distance"]) +temp = pd.DatetimeIndex(normalized_df["pickup_datetime"]) +normalized_df["pickup_date"] = temp.date +normalized_df["pickup_time"] = temp.time -time_split_df = (replaced_distance_vals_df - .split_column_by_example(source_column="pickup_datetime") - .split_column_by_example(source_column="dropoff_datetime")) +temp = pd.DatetimeIndex(normalized_df["dropoff_datetime"]) +normalized_df["dropoff_date"] = temp.date +normalized_df["dropoff_time"] = temp.time -# Split the pickup and dropoff datetime values into the respective date and time columns -renamed_col_df = (time_split_df - .rename_columns(column_pairs={ - "pickup_datetime_1": "pickup_date", - "pickup_datetime_2": "pickup_time", - "dropoff_datetime_1": "dropoff_date", - "dropoff_datetime_2": "dropoff_time"})) +del normalized_df["pickup_datetime"] +del normalized_df["dropoff_datetime"] + +normalized_df.reset_index(inplace=True, drop=True) if not (args.output_normalize is None): os.makedirs(args.output_normalize, exist_ok=True) print("%s created" % args.output_normalize) - write_df = renamed_col_df.write_to_csv(directory_path=dprep.LocalFileOutput(args.output_normalize)) - write_df.run_local() + path = args.output_normalize + "/processed.parquet" + write_df = normalized_df.to_parquet(path) diff --git a/how-to-use-azureml/machine-learning-pipelines/nyc-taxi-data-regression-model-building/scripts/prepdata/transform.py b/how-to-use-azureml/machine-learning-pipelines/nyc-taxi-data-regression-model-building/scripts/prepdata/transform.py index c2ac6e95..5584d6ab 100644 --- a/how-to-use-azureml/machine-learning-pipelines/nyc-taxi-data-regression-model-building/scripts/prepdata/transform.py +++ b/how-to-use-azureml/machine-learning-pipelines/nyc-taxi-data-regression-model-building/scripts/prepdata/transform.py @@ -1,22 +1,24 @@ import argparse import os -import azureml.dataprep as dprep +from azureml.core import Run print("Transforms the renamed taxi data to the required format") +run = Run.get_context() + +# To learn more about how to access dataset in your script, please +# see https://docs.microsoft.com/en-us/azure/machine-learning/how-to-train-with-datasets. +normalized_data = run.input_datasets['normalized_data'] +normalized_df = normalized_data.to_pandas_dataframe() + parser = argparse.ArgumentParser("transform") -parser.add_argument("--input_transform", type=str, help="renamed taxi data") parser.add_argument("--output_transform", type=str, help="transformed taxi data") args = parser.parse_args() -print("Argument 1(input taxi data path): %s" % args.input_transform) print("Argument 2(output final transformed taxi data): %s" % args.output_transform) -renamed_df = dprep.read_csv(args.input_transform + '/part-*') - # These functions transform the renamed data to be used finally for training. -# Visit https://docs.microsoft.com/en-us/azure/machine-learning/service/tutorial-data-prep for more details # Split the pickup and dropoff date further into the day of the week, day of the month, and month values. # To get the day of the week value, use the derive_column_by_example() function. @@ -27,62 +29,46 @@ renamed_df = dprep.read_csv(args.input_transform + '/part-*') # use the drop_columns() function to delete the original fields as the newly generated features are preferred. # Rename the rest of the fields to use meaningful descriptions. -transformed_features_df = (renamed_df - .derive_column_by_example( - source_columns="pickup_date", - new_column_name="pickup_weekday", - example_data=[("2009-01-04", "Sunday"), ("2013-08-22", "Thursday")]) - .derive_column_by_example( - source_columns="dropoff_date", - new_column_name="dropoff_weekday", - example_data=[("2013-08-22", "Thursday"), ("2013-11-03", "Sunday")]) +normalized_df = normalized_df.astype({"pickup_date": 'datetime64', "dropoff_date": 'datetime64', + "pickup_time": 'datetime64', "dropoff_time": 'datetime64', + "distance": 'float64', "cost": 'float64'}) - .split_column_by_example(source_column="pickup_time") - .split_column_by_example(source_column="dropoff_time") +normalized_df["pickup_weekday"] = normalized_df["pickup_date"].dt.dayofweek +normalized_df["pickup_month"] = normalized_df["pickup_date"].dt.month +normalized_df["pickup_monthday"] = normalized_df["pickup_date"].dt.day - .split_column_by_example(source_column="pickup_time_1") - .split_column_by_example(source_column="dropoff_time_1") - .drop_columns(columns=[ - "pickup_date", "pickup_time", "dropoff_date", "dropoff_time", - "pickup_date_1", "dropoff_date_1", "pickup_time_1", "dropoff_time_1"]) +normalized_df["dropoff_weekday"] = normalized_df["dropoff_date"].dt.dayofweek +normalized_df["dropoff_month"] = normalized_df["dropoff_date"].dt.month +normalized_df["dropoff_monthday"] = normalized_df["dropoff_date"].dt.day - .rename_columns(column_pairs={ - "pickup_date_2": "pickup_month", - "pickup_date_3": "pickup_monthday", - "pickup_time_1_1": "pickup_hour", - "pickup_time_1_2": "pickup_minute", - "pickup_time_2": "pickup_second", - "dropoff_date_2": "dropoff_month", - "dropoff_date_3": "dropoff_monthday", - "dropoff_time_1_1": "dropoff_hour", - "dropoff_time_1_2": "dropoff_minute", - "dropoff_time_2": "dropoff_second"})) +normalized_df["pickup_hour"] = normalized_df["pickup_time"].dt.hour +normalized_df["pickup_minute"] = normalized_df["pickup_time"].dt.minute +normalized_df["pickup_second"] = normalized_df["pickup_time"].dt.second -# Drop the pickup_datetime and dropoff_datetime columns because they're +normalized_df["dropoff_hour"] = normalized_df["dropoff_time"].dt.hour +normalized_df["dropoff_minute"] = normalized_df["dropoff_time"].dt.minute +normalized_df["dropoff_second"] = normalized_df["dropoff_time"].dt.second + +# Drop the pickup_date, dropoff_date, pickup_time, dropoff_time columns because they're # no longer needed (granular time features like hour, # minute and second are more useful for model training). -processed_df = transformed_features_df.drop_columns(columns=["pickup_datetime", "dropoff_datetime"]) +del normalized_df["pickup_date"] +del normalized_df["dropoff_date"] +del normalized_df["pickup_time"] +del normalized_df["dropoff_time"] -# Use the type inference functionality to automatically check the data type of each field, -# and display the inference results. -type_infer = processed_df.builders.set_column_types() -type_infer.learn() - -# The inference results look correct based on the data. Now apply the type conversions to the dataflow. -type_converted_df = type_infer.to_dataflow() - -# Before you package the dataflow, run two final filters on the data set. +# Before you package the dataset, run two final filters on the dataset. # To eliminate incorrectly captured data points, -# filter the dataflow on records where both the cost and distance variable values are greater than zero. +# filter the dataset on records where both the cost and distance variable values are greater than zero. # This step will significantly improve machine learning model accuracy, # because data points with a zero cost or distance represent major outliers that throw off prediction accuracy. -final_df = type_converted_df.filter(dprep.col("distance") > 0) -final_df = final_df.filter(dprep.col("cost") > 0) +final_df = normalized_df[(normalized_df.distance > 0) & (normalized_df.cost > 0)] +final_df.reset_index(inplace=True, drop=True) # Writing the final dataframe to use for training in the following steps if not (args.output_transform is None): os.makedirs(args.output_transform, exist_ok=True) print("%s created" % args.output_transform) - write_df = final_df.write_to_csv(directory_path=dprep.LocalFileOutput(args.output_transform)) - write_df.run_local() + path = args.output_transform + "/processed.parquet" + write_df = final_df.to_parquet(path) diff --git a/how-to-use-azureml/machine-learning-pipelines/nyc-taxi-data-regression-model-building/scripts/trainmodel/featurization.py b/how-to-use-azureml/machine-learning-pipelines/nyc-taxi-data-regression-model-building/scripts/trainmodel/featurization.py deleted file mode 100644 index bcf2338a..00000000 --- a/how-to-use-azureml/machine-learning-pipelines/nyc-taxi-data-regression-model-building/scripts/trainmodel/featurization.py +++ /dev/null @@ -1,31 +0,0 @@ -import argparse -import os -import azureml.dataprep as dprep -import azureml.core - -print("Extracts important features from prepared data") - -parser = argparse.ArgumentParser("featurization") -parser.add_argument("--input_featurization", type=str, help="input featurization") -parser.add_argument("--useful_columns", type=str, help="columns to use") -parser.add_argument("--output_featurization", type=str, help="output featurization") - -args = parser.parse_args() - -print("Argument 1(input training data path): %s" % args.input_featurization) -print("Argument 2(column features to use): %s" % str(args.useful_columns.strip("[]").split("\;"))) -print("Argument 3:(output featurized training data path) %s" % args.output_featurization) - -dflow_prepared = dprep.read_csv(args.input_featurization + '/part-*') - -# These functions extracts useful features for training -# Visit https://docs.microsoft.com/en-us/azure/machine-learning/service/tutorial-auto-train-models for more detail - -useful_columns = [s.strip().strip("'") for s in args.useful_columns.strip("[]").split("\;")] -dflow = dflow_prepared.keep_columns(useful_columns) - -if not (args.output_featurization is None): - os.makedirs(args.output_featurization, exist_ok=True) - print("%s created" % args.output_featurization) - write_df = dflow.write_to_csv(directory_path=dprep.LocalFileOutput(args.output_featurization)) - write_df.run_local() diff --git a/how-to-use-azureml/machine-learning-pipelines/nyc-taxi-data-regression-model-building/scripts/trainmodel/get_data.py b/how-to-use-azureml/machine-learning-pipelines/nyc-taxi-data-regression-model-building/scripts/trainmodel/get_data.py deleted file mode 100644 index 6472e46a..00000000 --- a/how-to-use-azureml/machine-learning-pipelines/nyc-taxi-data-regression-model-building/scripts/trainmodel/get_data.py +++ /dev/null @@ -1,12 +0,0 @@ - -import os -import pandas as pd - - -def get_data(): - print("In get_data") - print(os.environ['AZUREML_DATAREFERENCE_output_split_train_x']) - X_train = pd.read_csv(os.environ['AZUREML_DATAREFERENCE_output_split_train_x'] + "/part-00000", header=0) - y_train = pd.read_csv(os.environ['AZUREML_DATAREFERENCE_output_split_train_y'] + "/part-00000", header=0) - - return {"X": X_train.values, "y": y_train.values.flatten()} diff --git a/how-to-use-azureml/machine-learning-pipelines/nyc-taxi-data-regression-model-building/scripts/trainmodel/train_test_split.py b/how-to-use-azureml/machine-learning-pipelines/nyc-taxi-data-regression-model-building/scripts/trainmodel/train_test_split.py index cdc80b61..48571e64 100644 --- a/how-to-use-azureml/machine-learning-pipelines/nyc-taxi-data-regression-model-building/scripts/trainmodel/train_test_split.py +++ b/how-to-use-azureml/machine-learning-pipelines/nyc-taxi-data-regression-model-building/scripts/trainmodel/train_test_split.py @@ -1,48 +1,38 @@ import argparse import os -import azureml.dataprep as dprep import azureml.core +from azureml.core import Run from sklearn.model_selection import train_test_split def write_output(df, path): os.makedirs(path, exist_ok=True) print("%s created" % path) - df.to_csv(path + "/part-00000", index=False) + df.to_parquet(path + "/processed.parquet") print("Split the data into train and test") +run = Run.get_context() +transformed_data = run.input_datasets['transformed_data'] +transformed_df = transformed_data.to_pandas_dataframe() parser = argparse.ArgumentParser("split") -parser.add_argument("--input_split_features", type=str, help="input split features") -parser.add_argument("--input_split_labels", type=str, help="input split labels") -parser.add_argument("--output_split_train_x", type=str, help="output split train features") -parser.add_argument("--output_split_train_y", type=str, help="output split train labels") -parser.add_argument("--output_split_test_x", type=str, help="output split test features") -parser.add_argument("--output_split_test_y", type=str, help="output split test labels") +parser.add_argument("--output_split_train", type=str, help="output split train data") +parser.add_argument("--output_split_test", type=str, help="output split test data") args = parser.parse_args() -print("Argument 1(input taxi data features path): %s" % args.input_split_features) -print("Argument 2(input taxi data labels path): %s" % args.input_split_labels) -print("Argument 3(output training features split path): %s" % args.output_split_train_x) -print("Argument 4(output training labels split path): %s" % args.output_split_train_y) -print("Argument 5(output test features split path): %s" % args.output_split_test_x) -print("Argument 6(output test labels split path): %s" % args.output_split_test_y) - -x_df = dprep.read_csv(path=args.input_split_features, header=dprep.PromoteHeadersMode.GROUPED).to_pandas_dataframe() -y_df = dprep.read_csv(path=args.input_split_labels, header=dprep.PromoteHeadersMode.GROUPED).to_pandas_dataframe() +print("Argument 1(output training data split path): %s" % args.output_split_train) +print("Argument 2(output test data split path): %s" % args.output_split_test) # These functions splits the input features and labels into test and train data # Visit https://docs.microsoft.com/en-us/azure/machine-learning/service/tutorial-auto-train-models for more detail -x_train, x_test, y_train, y_test = train_test_split(x_df, y_df, test_size=0.2, random_state=223) +output_split_train, output_split_test = train_test_split(transformed_df, test_size=0.2, random_state=223) +output_split_train.reset_index(inplace=True, drop=True) +output_split_test.reset_index(inplace=True, drop=True) -if not (args.output_split_train_x is None and - args.output_split_test_x is None and - args.output_split_train_y is None and - args.output_split_test_y is None): - write_output(x_train, args.output_split_train_x) - write_output(y_train, args.output_split_train_y) - write_output(x_test, args.output_split_test_x) - write_output(y_test, args.output_split_test_y) +if not (args.output_split_train is None and + args.output_split_test is None): + write_output(output_split_train, args.output_split_train) + write_output(output_split_test, args.output_split_test) diff --git a/how-to-use-azureml/machine-learning-pipelines/pipeline-style-transfer/pipeline-style-transfer.ipynb b/how-to-use-azureml/machine-learning-pipelines/pipeline-style-transfer/pipeline-style-transfer.ipynb index 59cf1c12..0643b8a9 100644 --- a/how-to-use-azureml/machine-learning-pipelines/pipeline-style-transfer/pipeline-style-transfer.ipynb +++ b/how-to-use-azureml/machine-learning-pipelines/pipeline-style-transfer/pipeline-style-transfer.ipynb @@ -314,7 +314,7 @@ "cd = CondaDependencies()\n", "\n", "cd.add_channel(\"conda-forge\")\n", - "cd.add_conda_package(\"ffmpeg\")\n", + "cd.add_conda_package(\"ffmpeg==4.0.2\")\n", "\n", "# Runconfig\n", "amlcompute_run_config = RunConfiguration(conda_dependencies=cd)\n", @@ -334,8 +334,7 @@ "\n", "ffmpeg_images_ds_name = \"ffmpeg_images_data\"\n", "ffmpeg_images = PipelineData(name=\"ffmpeg_images\", datastore=default_datastore)\n", - "ffmpeg_images_file_dataset = ffmpeg_images.as_dataset()\n", - "ffmpeg_images_named_file_dataset = ffmpeg_images_file_dataset.as_named_input(ffmpeg_images_ds_name)" + "ffmpeg_images_file_dataset = ffmpeg_images.as_dataset()" ] }, { @@ -371,11 +370,11 @@ " script_name=\"process_video.py\",\n", " arguments=[\"--input_video\", orangutan_video,\n", " \"--output_audio\", ffmpeg_audio,\n", - " \"--output_images\", ffmpeg_images,\n", + " \"--output_images\", ffmpeg_images_file_dataset,\n", " ],\n", " compute_target=cpu_cluster,\n", " inputs=[orangutan_video],\n", - " outputs=[ffmpeg_images, ffmpeg_audio],\n", + " outputs=[ffmpeg_images_file_dataset, ffmpeg_audio],\n", " runconfig=amlcompute_run_config,\n", " source_directory=scripts_folder\n", ")\n", @@ -415,6 +414,7 @@ "parallel_cd.add_channel(\"pytorch\")\n", "parallel_cd.add_conda_package(\"pytorch\")\n", "parallel_cd.add_conda_package(\"torchvision\")\n", + "parallel_cd.add_conda_package(\"pillow<7\") # needed for torchvision==0.4.0\n", "\n", "styleenvironment = Environment(name=\"styleenvironment\")\n", "styleenvironment.python.conda_dependencies=parallel_cd\n", @@ -453,7 +453,7 @@ "\n", "distributed_style_transfer_step = ParallelRunStep(\n", " name=parallel_step_name,\n", - " inputs=[ffmpeg_images_named_file_dataset], # Input file share/blob container/file dataset\n", + " inputs=[ffmpeg_images_file_dataset], # Input file share/blob container/file dataset\n", " output=processed_images, # Output file share/blob container\n", " models=[mosaic_model, candy_model],\n", " tags = {'scenario': \"batch inference\", 'type': \"demo\"},\n", diff --git a/how-to-use-azureml/ml-frameworks/chainer/deployment/train-hyperparameter-tune-deploy-with-chainer/train-hyperparameter-tune-deploy-with-chainer.ipynb b/how-to-use-azureml/ml-frameworks/chainer/deployment/train-hyperparameter-tune-deploy-with-chainer/train-hyperparameter-tune-deploy-with-chainer.ipynb index a5ad0e1a..94ce595e 100644 --- a/how-to-use-azureml/ml-frameworks/chainer/deployment/train-hyperparameter-tune-deploy-with-chainer/train-hyperparameter-tune-deploy-with-chainer.ipynb +++ b/how-to-use-azureml/ml-frameworks/chainer/deployment/train-hyperparameter-tune-deploy-with-chainer/train-hyperparameter-tune-deploy-with-chainer.ipynb @@ -418,6 +418,15 @@ "hyperdrive_run.wait_for_completion(show_output=True)" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert(hyperdrive_run.get_status() == \"Completed\")" + ] + }, { "cell_type": "markdown", "metadata": {}, diff --git a/how-to-use-azureml/ml-frameworks/pytorch/deployment/train-hyperparameter-tune-deploy-with-pytorch/train-hyperparameter-tune-deploy-with-pytorch.ipynb b/how-to-use-azureml/ml-frameworks/pytorch/deployment/train-hyperparameter-tune-deploy-with-pytorch/train-hyperparameter-tune-deploy-with-pytorch.ipynb index 4e621cfe..936fb627 100644 --- a/how-to-use-azureml/ml-frameworks/pytorch/deployment/train-hyperparameter-tune-deploy-with-pytorch/train-hyperparameter-tune-deploy-with-pytorch.ipynb +++ b/how-to-use-azureml/ml-frameworks/pytorch/deployment/train-hyperparameter-tune-deploy-with-pytorch/train-hyperparameter-tune-deploy-with-pytorch.ipynb @@ -440,6 +440,15 @@ "hyperdrive_run.wait_for_completion(show_output=True)" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert(hyperdrive_run.get_status() == \"Completed\")" + ] + }, { "cell_type": "markdown", "metadata": {}, diff --git a/how-to-use-azureml/ml-frameworks/pytorch/training/mask-rcnn-object-detection/coco_eval.py b/how-to-use-azureml/ml-frameworks/pytorch/training/mask-rcnn-object-detection/coco_eval.py new file mode 100644 index 00000000..2f032132 --- /dev/null +++ b/how-to-use-azureml/ml-frameworks/pytorch/training/mask-rcnn-object-detection/coco_eval.py @@ -0,0 +1,350 @@ +import json +import tempfile + +import numpy as np +import copy +import time +import torch +import torch._six + +from pycocotools.cocoeval import COCOeval +from pycocotools.coco import COCO +import pycocotools.mask as mask_util + +from collections import defaultdict + +import utils + + +class CocoEvaluator(object): + def __init__(self, coco_gt, iou_types): + assert isinstance(iou_types, (list, tuple)) + coco_gt = copy.deepcopy(coco_gt) + self.coco_gt = coco_gt + + self.iou_types = iou_types + self.coco_eval = {} + for iou_type in iou_types: + self.coco_eval[iou_type] = COCOeval(coco_gt, iouType=iou_type) + + self.img_ids = [] + self.eval_imgs = {k: [] for k in iou_types} + + def update(self, predictions): + img_ids = list(np.unique(list(predictions.keys()))) + self.img_ids.extend(img_ids) + + for iou_type in self.iou_types: + results = self.prepare(predictions, iou_type) + coco_dt = loadRes(self.coco_gt, results) if results else COCO() + coco_eval = self.coco_eval[iou_type] + + coco_eval.cocoDt = coco_dt + coco_eval.params.imgIds = list(img_ids) + img_ids, eval_imgs = evaluate(coco_eval) + + self.eval_imgs[iou_type].append(eval_imgs) + + def synchronize_between_processes(self): + for iou_type in self.iou_types: + self.eval_imgs[iou_type] = np.concatenate(self.eval_imgs[iou_type], 2) + create_common_coco_eval(self.coco_eval[iou_type], self.img_ids, self.eval_imgs[iou_type]) + + def accumulate(self): + for coco_eval in self.coco_eval.values(): + coco_eval.accumulate() + + def summarize(self): + for iou_type, coco_eval in self.coco_eval.items(): + print("IoU metric: {}".format(iou_type)) + coco_eval.summarize() + + def prepare(self, predictions, iou_type): + if iou_type == "bbox": + return self.prepare_for_coco_detection(predictions) + elif iou_type == "segm": + return self.prepare_for_coco_segmentation(predictions) + elif iou_type == "keypoints": + return self.prepare_for_coco_keypoint(predictions) + else: + raise ValueError("Unknown iou type {}".format(iou_type)) + + def prepare_for_coco_detection(self, predictions): + coco_results = [] + for original_id, prediction in predictions.items(): + if len(prediction) == 0: + continue + + boxes = prediction["boxes"] + boxes = convert_to_xywh(boxes).tolist() + scores = prediction["scores"].tolist() + labels = prediction["labels"].tolist() + + coco_results.extend( + [ + { + "image_id": original_id, + "category_id": labels[k], + "bbox": box, + "score": scores[k], + } + for k, box in enumerate(boxes) + ] + ) + return coco_results + + def prepare_for_coco_segmentation(self, predictions): + coco_results = [] + for original_id, prediction in predictions.items(): + if len(prediction) == 0: + continue + + scores = prediction["scores"] + labels = prediction["labels"] + masks = prediction["masks"] + + masks = masks > 0.5 + + scores = prediction["scores"].tolist() + labels = prediction["labels"].tolist() + + rles = [ + mask_util.encode(np.array(mask[0, :, :, np.newaxis], dtype=np.uint8, order="F"))[0] + for mask in masks + ] + for rle in rles: + rle["counts"] = rle["counts"].decode("utf-8") + + coco_results.extend( + [ + { + "image_id": original_id, + "category_id": labels[k], + "segmentation": rle, + "score": scores[k], + } + for k, rle in enumerate(rles) + ] + ) + return coco_results + + def prepare_for_coco_keypoint(self, predictions): + coco_results = [] + for original_id, prediction in predictions.items(): + if len(prediction) == 0: + continue + + boxes = prediction["boxes"] + boxes = convert_to_xywh(boxes).tolist() + scores = prediction["scores"].tolist() + labels = prediction["labels"].tolist() + keypoints = prediction["keypoints"] + keypoints = keypoints.flatten(start_dim=1).tolist() + + coco_results.extend( + [ + { + "image_id": original_id, + "category_id": labels[k], + 'keypoints': keypoint, + "score": scores[k], + } + for k, keypoint in enumerate(keypoints) + ] + ) + return coco_results + + +def convert_to_xywh(boxes): + xmin, ymin, xmax, ymax = boxes.unbind(1) + return torch.stack((xmin, ymin, xmax - xmin, ymax - ymin), dim=1) + + +def merge(img_ids, eval_imgs): + all_img_ids = utils.all_gather(img_ids) + all_eval_imgs = utils.all_gather(eval_imgs) + + merged_img_ids = [] + for p in all_img_ids: + merged_img_ids.extend(p) + + merged_eval_imgs = [] + for p in all_eval_imgs: + merged_eval_imgs.append(p) + + merged_img_ids = np.array(merged_img_ids) + merged_eval_imgs = np.concatenate(merged_eval_imgs, 2) + + # keep only unique (and in sorted order) images + merged_img_ids, idx = np.unique(merged_img_ids, return_index=True) + merged_eval_imgs = merged_eval_imgs[..., idx] + + return merged_img_ids, merged_eval_imgs + + +def create_common_coco_eval(coco_eval, img_ids, eval_imgs): + img_ids, eval_imgs = merge(img_ids, eval_imgs) + img_ids = list(img_ids) + eval_imgs = list(eval_imgs.flatten()) + + coco_eval.evalImgs = eval_imgs + coco_eval.params.imgIds = img_ids + coco_eval._paramsEval = copy.deepcopy(coco_eval.params) + + +################################################################# +# From pycocotools, just removed the prints and fixed +# a Python3 bug about unicode not defined +################################################################# + +# Ideally, pycocotools wouldn't have hard-coded prints +# so that we could avoid copy-pasting those two functions + +def createIndex(self): + # create index + # print('creating index...') + anns, cats, imgs = {}, {}, {} + imgToAnns, catToImgs = defaultdict(list), defaultdict(list) + if 'annotations' in self.dataset: + for ann in self.dataset['annotations']: + imgToAnns[ann['image_id']].append(ann) + anns[ann['id']] = ann + + if 'images' in self.dataset: + for img in self.dataset['images']: + imgs[img['id']] = img + + if 'categories' in self.dataset: + for cat in self.dataset['categories']: + cats[cat['id']] = cat + + if 'annotations' in self.dataset and 'categories' in self.dataset: + for ann in self.dataset['annotations']: + catToImgs[ann['category_id']].append(ann['image_id']) + + # print('index created!') + + # create class members + self.anns = anns + self.imgToAnns = imgToAnns + self.catToImgs = catToImgs + self.imgs = imgs + self.cats = cats + + +maskUtils = mask_util + + +def loadRes(self, resFile): + """ + Load result file and return a result api object. + :param resFile (str) : file name of result file + :return: res (obj) : result api object + """ + res = COCO() + res.dataset['images'] = [img for img in self.dataset['images']] + + # print('Loading and preparing results...') + # tic = time.time() + if isinstance(resFile, torch._six.string_classes): + anns = json.load(open(resFile)) + elif type(resFile) == np.ndarray: + anns = self.loadNumpyAnnotations(resFile) + else: + anns = resFile + assert type(anns) == list, 'results in not an array of objects' + annsImgIds = [ann['image_id'] for ann in anns] + assert set(annsImgIds) == (set(annsImgIds) & set(self.getImgIds())), \ + 'Results do not correspond to current coco set' + if 'caption' in anns[0]: + imgIds = set([img['id'] for img in res.dataset['images']]) & set([ann['image_id'] for ann in anns]) + res.dataset['images'] = [img for img in res.dataset['images'] if img['id'] in imgIds] + for id, ann in enumerate(anns): + ann['id'] = id + 1 + elif 'bbox' in anns[0] and not anns[0]['bbox'] == []: + res.dataset['categories'] = copy.deepcopy(self.dataset['categories']) + for id, ann in enumerate(anns): + bb = ann['bbox'] + x1, x2, y1, y2 = [bb[0], bb[0] + bb[2], bb[1], bb[1] + bb[3]] + if 'segmentation' not in ann: + ann['segmentation'] = [[x1, y1, x1, y2, x2, y2, x2, y1]] + ann['area'] = bb[2] * bb[3] + ann['id'] = id + 1 + ann['iscrowd'] = 0 + elif 'segmentation' in anns[0]: + res.dataset['categories'] = copy.deepcopy(self.dataset['categories']) + for id, ann in enumerate(anns): + # now only support compressed RLE format as segmentation results + ann['area'] = maskUtils.area(ann['segmentation']) + if 'bbox' not in ann: + ann['bbox'] = maskUtils.toBbox(ann['segmentation']) + ann['id'] = id + 1 + ann['iscrowd'] = 0 + elif 'keypoints' in anns[0]: + res.dataset['categories'] = copy.deepcopy(self.dataset['categories']) + for id, ann in enumerate(anns): + s = ann['keypoints'] + x = s[0::3] + y = s[1::3] + x1, x2, y1, y2 = np.min(x), np.max(x), np.min(y), np.max(y) + ann['area'] = (x2 - x1) * (y2 - y1) + ann['id'] = id + 1 + ann['bbox'] = [x1, y1, x2 - x1, y2 - y1] + # print('DONE (t={:0.2f}s)'.format(time.time()- tic)) + + res.dataset['annotations'] = anns + createIndex(res) + return res + + +def evaluate(self): + ''' + Run per image evaluation on given images and store results (a list of dict) in self.evalImgs + :return: None + ''' + # tic = time.time() + # print('Running per image evaluation...') + p = self.params + # add backward compatibility if useSegm is specified in params + if p.useSegm is not None: + p.iouType = 'segm' if p.useSegm == 1 else 'bbox' + print('useSegm (deprecated) is not None. Running {} evaluation'.format(p.iouType)) + # print('Evaluate annotation type *{}*'.format(p.iouType)) + p.imgIds = list(np.unique(p.imgIds)) + if p.useCats: + p.catIds = list(np.unique(p.catIds)) + p.maxDets = sorted(p.maxDets) + self.params = p + + self._prepare() + # loop through images, area range, max detection number + catIds = p.catIds if p.useCats else [-1] + + if p.iouType == 'segm' or p.iouType == 'bbox': + computeIoU = self.computeIoU + elif p.iouType == 'keypoints': + computeIoU = self.computeOks + self.ious = { + (imgId, catId): computeIoU(imgId, catId) + for imgId in p.imgIds + for catId in catIds} + + evaluateImg = self.evaluateImg + maxDet = p.maxDets[-1] + evalImgs = [ + evaluateImg(imgId, catId, areaRng, maxDet) + for catId in catIds + for areaRng in p.areaRng + for imgId in p.imgIds + ] + # this is NOT in the pycocotools code, but could be done outside + evalImgs = np.asarray(evalImgs).reshape( + len(catIds), len(p.areaRng), len(p.imgIds)) + self._paramsEval = copy.deepcopy(self.params) + # toc = time.time() + # print('DONE (t={:0.2f}s).'.format(toc-tic)) + return p.imgIds, evalImgs + +################################################################# +# end of straight copy from pycocotools, just removing the prints +################################################################# diff --git a/how-to-use-azureml/ml-frameworks/pytorch/training/mask-rcnn-object-detection/coco_utils.py b/how-to-use-azureml/ml-frameworks/pytorch/training/mask-rcnn-object-detection/coco_utils.py new file mode 100644 index 00000000..26701a2c --- /dev/null +++ b/how-to-use-azureml/ml-frameworks/pytorch/training/mask-rcnn-object-detection/coco_utils.py @@ -0,0 +1,252 @@ +import copy +import os +from PIL import Image + +import torch +import torch.utils.data +import torchvision + +from pycocotools import mask as coco_mask +from pycocotools.coco import COCO + +import transforms as T + + +class FilterAndRemapCocoCategories(object): + def __init__(self, categories, remap=True): + self.categories = categories + self.remap = remap + + def __call__(self, image, target): + anno = target["annotations"] + anno = [obj for obj in anno if obj["category_id"] in self.categories] + if not self.remap: + target["annotations"] = anno + return image, target + anno = copy.deepcopy(anno) + for obj in anno: + obj["category_id"] = self.categories.index(obj["category_id"]) + target["annotations"] = anno + return image, target + + +def convert_coco_poly_to_mask(segmentations, height, width): + masks = [] + for polygons in segmentations: + rles = coco_mask.frPyObjects(polygons, height, width) + mask = coco_mask.decode(rles) + if len(mask.shape) < 3: + mask = mask[..., None] + mask = torch.as_tensor(mask, dtype=torch.uint8) + mask = mask.any(dim=2) + masks.append(mask) + if masks: + masks = torch.stack(masks, dim=0) + else: + masks = torch.zeros((0, height, width), dtype=torch.uint8) + return masks + + +class ConvertCocoPolysToMask(object): + def __call__(self, image, target): + w, h = image.size + + image_id = target["image_id"] + image_id = torch.tensor([image_id]) + + anno = target["annotations"] + + anno = [obj for obj in anno if obj['iscrowd'] == 0] + + boxes = [obj["bbox"] for obj in anno] + # guard against no boxes via resizing + boxes = torch.as_tensor(boxes, dtype=torch.float32).reshape(-1, 4) + boxes[:, 2:] += boxes[:, :2] + boxes[:, 0::2].clamp_(min=0, max=w) + boxes[:, 1::2].clamp_(min=0, max=h) + + classes = [obj["category_id"] for obj in anno] + classes = torch.tensor(classes, dtype=torch.int64) + + segmentations = [obj["segmentation"] for obj in anno] + masks = convert_coco_poly_to_mask(segmentations, h, w) + + keypoints = None + if anno and "keypoints" in anno[0]: + keypoints = [obj["keypoints"] for obj in anno] + keypoints = torch.as_tensor(keypoints, dtype=torch.float32) + num_keypoints = keypoints.shape[0] + if num_keypoints: + keypoints = keypoints.view(num_keypoints, -1, 3) + + keep = (boxes[:, 3] > boxes[:, 1]) & (boxes[:, 2] > boxes[:, 0]) + boxes = boxes[keep] + classes = classes[keep] + masks = masks[keep] + if keypoints is not None: + keypoints = keypoints[keep] + + target = {} + target["boxes"] = boxes + target["labels"] = classes + target["masks"] = masks + target["image_id"] = image_id + if keypoints is not None: + target["keypoints"] = keypoints + + # for conversion to coco api + area = torch.tensor([obj["area"] for obj in anno]) + iscrowd = torch.tensor([obj["iscrowd"] for obj in anno]) + target["area"] = area + target["iscrowd"] = iscrowd + + return image, target + + +def _coco_remove_images_without_annotations(dataset, cat_list=None): + def _has_only_empty_bbox(anno): + return all(any(o <= 1 for o in obj["bbox"][2:]) for obj in anno) + + def _count_visible_keypoints(anno): + return sum(sum(1 for v in ann["keypoints"][2::3] if v > 0) for ann in anno) + + min_keypoints_per_image = 10 + + def _has_valid_annotation(anno): + # if it's empty, there is no annotation + if len(anno) == 0: + return False + # if all boxes have close to zero area, there is no annotation + if _has_only_empty_bbox(anno): + return False + # keypoints task have a slight different critera for considering + # if an annotation is valid + if "keypoints" not in anno[0]: + return True + # for keypoint detection tasks, only consider valid images those + # containing at least min_keypoints_per_image + if _count_visible_keypoints(anno) >= min_keypoints_per_image: + return True + return False + + assert isinstance(dataset, torchvision.datasets.CocoDetection) + ids = [] + for ds_idx, img_id in enumerate(dataset.ids): + ann_ids = dataset.coco.getAnnIds(imgIds=img_id, iscrowd=None) + anno = dataset.coco.loadAnns(ann_ids) + if cat_list: + anno = [obj for obj in anno if obj["category_id"] in cat_list] + if _has_valid_annotation(anno): + ids.append(ds_idx) + + dataset = torch.utils.data.Subset(dataset, ids) + return dataset + + +def convert_to_coco_api(ds): + coco_ds = COCO() + # annotation IDs need to start at 1, not 0, see torchvision issue #1530 + ann_id = 1 + dataset = {'images': [], 'categories': [], 'annotations': []} + categories = set() + for img_idx in range(len(ds)): + # find better way to get target + # targets = ds.get_annotations(img_idx) + img, targets = ds[img_idx] + image_id = targets["image_id"].item() + img_dict = {} + img_dict['id'] = image_id + img_dict['height'] = img.shape[-2] + img_dict['width'] = img.shape[-1] + dataset['images'].append(img_dict) + bboxes = targets["boxes"] + bboxes[:, 2:] -= bboxes[:, :2] + bboxes = bboxes.tolist() + labels = targets['labels'].tolist() + areas = targets['area'].tolist() + iscrowd = targets['iscrowd'].tolist() + if 'masks' in targets: + masks = targets['masks'] + # make masks Fortran contiguous for coco_mask + masks = masks.permute(0, 2, 1).contiguous().permute(0, 2, 1) + if 'keypoints' in targets: + keypoints = targets['keypoints'] + keypoints = keypoints.reshape(keypoints.shape[0], -1).tolist() + num_objs = len(bboxes) + for i in range(num_objs): + ann = {} + ann['image_id'] = image_id + ann['bbox'] = bboxes[i] + ann['category_id'] = labels[i] + categories.add(labels[i]) + ann['area'] = areas[i] + ann['iscrowd'] = iscrowd[i] + ann['id'] = ann_id + if 'masks' in targets: + ann["segmentation"] = coco_mask.encode(masks[i].numpy()) + if 'keypoints' in targets: + ann['keypoints'] = keypoints[i] + ann['num_keypoints'] = sum(k != 0 for k in keypoints[i][2::3]) + dataset['annotations'].append(ann) + ann_id += 1 + dataset['categories'] = [{'id': i} for i in sorted(categories)] + coco_ds.dataset = dataset + coco_ds.createIndex() + return coco_ds + + +def get_coco_api_from_dataset(dataset): + for _ in range(10): + if isinstance(dataset, torchvision.datasets.CocoDetection): + break + if isinstance(dataset, torch.utils.data.Subset): + dataset = dataset.dataset + if isinstance(dataset, torchvision.datasets.CocoDetection): + return dataset.coco + return convert_to_coco_api(dataset) + + +class CocoDetection(torchvision.datasets.CocoDetection): + def __init__(self, img_folder, ann_file, transforms): + super(CocoDetection, self).__init__(img_folder, ann_file) + self._transforms = transforms + + def __getitem__(self, idx): + img, target = super(CocoDetection, self).__getitem__(idx) + image_id = self.ids[idx] + target = dict(image_id=image_id, annotations=target) + if self._transforms is not None: + img, target = self._transforms(img, target) + return img, target + + +def get_coco(root, image_set, transforms, mode='instances'): + anno_file_template = "{}_{}2017.json" + PATHS = { + "train": ("train2017", os.path.join("annotations", anno_file_template.format(mode, "train"))), + "val": ("val2017", os.path.join("annotations", anno_file_template.format(mode, "val"))), + # "train": ("val2017", os.path.join("annotations", anno_file_template.format(mode, "val"))) + } + + t = [ConvertCocoPolysToMask()] + + if transforms is not None: + t.append(transforms) + transforms = T.Compose(t) + + img_folder, ann_file = PATHS[image_set] + img_folder = os.path.join(root, img_folder) + ann_file = os.path.join(root, ann_file) + + dataset = CocoDetection(img_folder, ann_file, transforms=transforms) + + if image_set == "train": + dataset = _coco_remove_images_without_annotations(dataset) + + # dataset = torch.utils.data.Subset(dataset, [i for i in range(500)]) + + return dataset + + +def get_coco_kp(root, image_set, transforms): + return get_coco(root, image_set, transforms, mode="person_keypoints") diff --git a/how-to-use-azureml/ml-frameworks/pytorch/training/mask-rcnn-object-detection/data.py b/how-to-use-azureml/ml-frameworks/pytorch/training/mask-rcnn-object-detection/data.py new file mode 100644 index 00000000..6b8ee492 --- /dev/null +++ b/how-to-use-azureml/ml-frameworks/pytorch/training/mask-rcnn-object-detection/data.py @@ -0,0 +1,77 @@ +import numpy as np +import os +import torch.utils.data + +from azureml.core import Run +from PIL import Image + + +class PennFudanDataset(torch.utils.data.Dataset): + def __init__(self, root, transforms=None): + self.root = root + self.transforms = transforms + + # load all image files, sorting them to ensure that they are aligned + self.img_dir = os.path.join(root, "PNGImages") + self.mask_dir = os.path.join(root, "PedMasks") + + self.imgs = list(sorted(os.listdir(self.img_dir))) + self.masks = list(sorted(os.listdir(self.mask_dir))) + + def __getitem__(self, idx): + # load images ad masks + img_path = os.path.join(self.img_dir, self.imgs[idx]) + mask_path = os.path.join(self.mask_dir, self.masks[idx]) + + img = Image.open(img_path).convert("RGB") + # note that we haven't converted the mask to RGB, + # because each color corresponds to a different instance + # with 0 being background + mask = Image.open(mask_path) + + mask = np.array(mask) + # instances are encoded as different colors + obj_ids = np.unique(mask) + # first id is the background, so remove it + obj_ids = obj_ids[1:] + + # split the color-encoded mask into a set + # of binary masks + masks = mask == obj_ids[:, None, None] + + # get bounding box coordinates for each mask + num_objs = len(obj_ids) + boxes = [] + for i in range(num_objs): + pos = np.where(masks[i]) + xmin = np.min(pos[1]) + xmax = np.max(pos[1]) + ymin = np.min(pos[0]) + ymax = np.max(pos[0]) + boxes.append([xmin, ymin, xmax, ymax]) + + boxes = torch.as_tensor(boxes, dtype=torch.float32) + # there is only one class + labels = torch.ones((num_objs,), dtype=torch.int64) + masks = torch.as_tensor(masks, dtype=torch.uint8) + + image_id = torch.tensor([idx]) + area = (boxes[:, 3] - boxes[:, 1]) * (boxes[:, 2] - boxes[:, 0]) + # suppose all instances are not crowd + iscrowd = torch.zeros((num_objs,), dtype=torch.int64) + + target = {} + target["boxes"] = boxes + target["labels"] = labels + target["masks"] = masks + target["image_id"] = image_id + target["area"] = area + target["iscrowd"] = iscrowd + + if self.transforms is not None: + img, target = self.transforms(img, target) + + return img, target + + def __len__(self): + return len(self.imgs) diff --git a/how-to-use-azureml/ml-frameworks/pytorch/training/mask-rcnn-object-detection/dockerfiles/Dockerfile b/how-to-use-azureml/ml-frameworks/pytorch/training/mask-rcnn-object-detection/dockerfiles/Dockerfile new file mode 100644 index 00000000..9b76f600 --- /dev/null +++ b/how-to-use-azureml/ml-frameworks/pytorch/training/mask-rcnn-object-detection/dockerfiles/Dockerfile @@ -0,0 +1,16 @@ +# From https://github.com/microsoft/AzureML-BERT/blob/master/finetune/PyTorch/dockerfile + +FROM mcr.microsoft.com/azureml/base-gpu:openmpi3.1.2-cuda10.1-cudnn7-ubuntu18.04 + +RUN apt update && apt install git -y && rm -rf /var/lib/apt/lists/* + +RUN /opt/miniconda/bin/conda update -n base -c defaults conda +RUN /opt/miniconda/bin/conda install -y cython=0.29.15 numpy=1.18.1 +RUN /opt/miniconda/bin/conda install -y pytorch=1.4 torchvision=0.5.0 -c pytorch + +# Install cocoapi, required for drawing bounding boxes +RUN git clone https://github.com/cocodataset/cocoapi.git && cd cocoapi/PythonAPI && python setup.py build_ext install + +RUN pip install azureml-defaults +RUN pip install "azureml-dataprep[fuse]" +RUN pip install pandas pyarrow diff --git a/how-to-use-azureml/ml-frameworks/pytorch/training/mask-rcnn-object-detection/engine.py b/how-to-use-azureml/ml-frameworks/pytorch/training/mask-rcnn-object-detection/engine.py new file mode 100644 index 00000000..68c39a4f --- /dev/null +++ b/how-to-use-azureml/ml-frameworks/pytorch/training/mask-rcnn-object-detection/engine.py @@ -0,0 +1,108 @@ +import math +import sys +import time +import torch + +import torchvision.models.detection.mask_rcnn + +from coco_utils import get_coco_api_from_dataset +from coco_eval import CocoEvaluator +import utils + + +def train_one_epoch(model, optimizer, data_loader, device, epoch, print_freq): + model.train() + metric_logger = utils.MetricLogger(delimiter=" ") + metric_logger.add_meter('lr', utils.SmoothedValue(window_size=1, fmt='{value:.6f}')) + header = 'Epoch: [{}]'.format(epoch) + + lr_scheduler = None + if epoch == 0: + warmup_factor = 1. / 1000 + warmup_iters = min(1000, len(data_loader) - 1) + + lr_scheduler = utils.warmup_lr_scheduler(optimizer, warmup_iters, warmup_factor) + + for images, targets in metric_logger.log_every(data_loader, print_freq, header): + images = list(image.to(device) for image in images) + targets = [{k: v.to(device) for k, v in t.items()} for t in targets] + + loss_dict = model(images, targets) + + losses = sum(loss for loss in loss_dict.values()) + + # reduce losses over all GPUs for logging purposes + loss_dict_reduced = utils.reduce_dict(loss_dict) + losses_reduced = sum(loss for loss in loss_dict_reduced.values()) + + loss_value = losses_reduced.item() + + if not math.isfinite(loss_value): + print("Loss is {}, stopping training".format(loss_value)) + print(loss_dict_reduced) + sys.exit(1) + + optimizer.zero_grad() + losses.backward() + optimizer.step() + + if lr_scheduler is not None: + lr_scheduler.step() + + metric_logger.update(loss=losses_reduced, **loss_dict_reduced) + metric_logger.update(lr=optimizer.param_groups[0]["lr"]) + + +def _get_iou_types(model): + model_without_ddp = model + if isinstance(model, torch.nn.parallel.DistributedDataParallel): + model_without_ddp = model.module + iou_types = ["bbox"] + if isinstance(model_without_ddp, torchvision.models.detection.MaskRCNN): + iou_types.append("segm") + if isinstance(model_without_ddp, torchvision.models.detection.KeypointRCNN): + iou_types.append("keypoints") + return iou_types + + +@torch.no_grad() +def evaluate(model, data_loader, device): + n_threads = torch.get_num_threads() + # FIXME remove this and make paste_masks_in_image run on the GPU + torch.set_num_threads(1) + cpu_device = torch.device("cpu") + model.eval() + metric_logger = utils.MetricLogger(delimiter=" ") + header = 'Test:' + + coco = get_coco_api_from_dataset(data_loader.dataset) + iou_types = _get_iou_types(model) + coco_evaluator = CocoEvaluator(coco, iou_types) + + for image, targets in metric_logger.log_every(data_loader, 100, header): + image = list(img.to(device) for img in image) + targets = [{k: v.to(device) for k, v in t.items()} for t in targets] + + torch.cuda.synchronize() + model_time = time.time() + outputs = model(image) + + outputs = [{k: v.to(cpu_device) for k, v in t.items()} for t in outputs] + model_time = time.time() - model_time + + res = {target["image_id"].item(): output for target, output in zip(targets, outputs)} + evaluator_time = time.time() + coco_evaluator.update(res) + evaluator_time = time.time() - evaluator_time + metric_logger.update(model_time=model_time, evaluator_time=evaluator_time) + + # gather the stats from all processes + metric_logger.synchronize_between_processes() + print("Averaged stats:", metric_logger) + coco_evaluator.synchronize_between_processes() + + # accumulate predictions from all images + coco_evaluator.accumulate() + coco_evaluator.summarize() + torch.set_num_threads(n_threads) + return coco_evaluator diff --git a/how-to-use-azureml/ml-frameworks/pytorch/training/mask-rcnn-object-detection/model.py b/how-to-use-azureml/ml-frameworks/pytorch/training/mask-rcnn-object-detection/model.py new file mode 100644 index 00000000..12e32eff --- /dev/null +++ b/how-to-use-azureml/ml-frameworks/pytorch/training/mask-rcnn-object-detection/model.py @@ -0,0 +1,23 @@ +import torchvision + +from torchvision.models.detection.faster_rcnn import FastRCNNPredictor +from torchvision.models.detection.mask_rcnn import MaskRCNNPredictor + + +def get_instance_segmentation_model(num_classes): + # load an instance segmentation model pre-trained on COCO + model = torchvision.models.detection.maskrcnn_resnet50_fpn(pretrained=True) + + # get the number of input features for the classifier + in_features = model.roi_heads.box_predictor.cls_score.in_features + # replace the pre-trained head with a new one + model.roi_heads.box_predictor = FastRCNNPredictor(in_features, num_classes) + + # now get the number of input features for the mask classifier + in_features_mask = model.roi_heads.mask_predictor.conv5_mask.in_channels + hidden_layer = 256 + # and replace the mask predictor with a new one + model.roi_heads.mask_predictor = MaskRCNNPredictor(in_features_mask, + hidden_layer, + num_classes) + return model diff --git a/how-to-use-azureml/ml-frameworks/pytorch/training/mask-rcnn-object-detection/pytorch-mask-rcnn.ipynb b/how-to-use-azureml/ml-frameworks/pytorch/training/mask-rcnn-object-detection/pytorch-mask-rcnn.ipynb new file mode 100644 index 00000000..e21a40ab --- /dev/null +++ b/how-to-use-azureml/ml-frameworks/pytorch/training/mask-rcnn-object-detection/pytorch-mask-rcnn.ipynb @@ -0,0 +1,544 @@ +{ + "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": [ + "![Impressions](https://PixelServer20190423114238.azurewebsites.net/api/impressions/MachineLearningNotebooks/how-to-use-azureml/ml-frameworks/pytorch/training/mask-rcnn-object-detection/pytorch-mask-rcnn.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Object detection with PyTorch, Mask R-CNN, and a custom Dockerfile\n", + "\n", + "In this tutorial, you will finetune a pre-trained [Mask R-CNN](https://arxiv.org/abs/1703.06870) model on images from the [Penn-Fudan Database for Pedestrian Detection and Segmentation](https://www.cis.upenn.edu/~jshi/ped_html/). The dataset has 170 images with 345 instances of pedestrians. After running this tutorial, you will have a model that can outline the silhouettes of all pedestrians within an image.\n", + "\n", + "You\u00e2\u20ac\u2122ll use Azure Machine Learning to: \n", + "\n", + "- Initialize a workspace \n", + "- Create a compute cluster\n", + "- Define a training environment\n", + "- Train a model remotely\n", + "- Register your model\n", + "- Generate predictions locally\n", + "\n", + "## Prerequisities\n", + "\n", + "- If you are using an Azure Machine Learning Notebook VM, your environment already meets these prerequisites. Otherwise, go through the [configuration notebook](../../../../../configuration.ipynb) to install the Azure Machine Learning Python SDK and [create an Azure ML Workspace](https://docs.microsoft.com/azure/machine-learning/how-to-manage-workspace#create-a-workspace). You also need matplotlib 3.2, pycocotools-2.0.0, torchvision >= 0.5.0 and torch >= 1.4.0.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Check core SDK version number, check other dependencies\n", + "import azureml.core\n", + "import matplotlib\n", + "import pycocotools\n", + "import torch\n", + "import torchvision\n", + "\n", + "print(\"SDK version:\", azureml.core.VERSION)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Diagnostics\n", + "\n", + "Opt-in diagnostics for better experience, quality, and security in future releases." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from azureml.telemetry import set_diagnostics_collection\n", + "\n", + "set_diagnostics_collection(send_diagnostics=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Initialize a workspace\n", + "\n", + "Initialize a [workspace](https://docs.microsoft.com/en-us/azure/machine-learning/concept-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`, using the [from_config()](https://docs.microsoft.com/python/api/azureml-core/azureml.core.workspace(class)?view=azure-ml-py#from-config-path-none--auth-none---logger-none---file-name-none-) method." + ] + }, + { + "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 or attach existing Azure ML Managed Compute\n", + "\n", + "You will need to create a [compute target](https://docs.microsoft.com/azure/machine-learning/concept-compute-target) for training your model. In this tutorial, we use [Azure ML managed compute](https://docs.microsoft.com/azure/machine-learning/how-to-set-up-training-targets#amlcompute) for our remote training compute resource. Specifically, the below code creates a `STANDARD_NC6` GPU cluster that autoscales from 0 to 4 nodes.\n", + "\n", + "**Creation of Compute takes approximately 5 minutes.** If the Aauzre ML Compute with that name is already in your workspace, this code will skip the creation process. \n", + "\n", + "As with other Azure servies, there are limits on certain resources associated with the Azure Machine Learning service. Please read [this article](https://docs.microsoft.com/azure/machine-learning/how-to-manage-quotas) on the default limits and how to request more quota.\n", + "\n", + "> Note that the below code creates GPU compute. If you instead want to create CPU compute, provide a different VM size to the `vm_size` parameter, such as `STANDARD_D2_V2`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from azureml.core.compute import ComputeTarget, AmlCompute\n", + "from azureml.core.compute_target import ComputeTargetException\n", + "\n", + "\n", + "# choose a name for your cluster\n", + "cluster_name = 'gpu-cluster'\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 = AmlCompute.provisioning_configuration(vm_size='STANDARD_NC6', \n", + " 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 get_status() to get a detailed status for the current cluster. \n", + "print(compute_target.get_status().serialize())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Define a training environment\n", + "\n", + "### Create a project directory\n", + "Create a directory that will contain all the code from your local machine that you will need access to on the remote resource. This includes the training script an any additional files your training script depends on." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "\n", + "project_folder = './pytorch-peds'\n", + "\n", + "try:\n", + " os.makedirs(project_folder, exist_ok=False)\n", + "except FileExistsError:\n", + " print('project folder {} exists, moving on...'.format(project_folder))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Copy training script and dependencies into project directory" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import shutil\n", + "\n", + "files_to_copy = ['data', 'model', 'script', 'utils', 'transforms', 'coco_eval', 'engine', 'coco_utils']\n", + "for file in files_to_copy:\n", + " shutil.copy(os.path.join(os.getcwd(), (file + '.py')), project_folder)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Create an experiment" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from azureml.core import Experiment\n", + "\n", + "experiment_name = 'pytorch-peds'\n", + "experiment = Experiment(ws, name=experiment_name)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Specify dependencies with a custom Dockerfile\n", + "\n", + "There are a number of ways to [use environments](https://docs.microsoft.com/azure/machine-learning/how-to-use-environments) for specifying dependencies during model training. In this case, we use a custom Dockerfile." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from azureml.core import Environment\n", + "\n", + "my_env = Environment(name='maskr-docker')\n", + "my_env.docker.enabled = True\n", + "with open(\"dockerfiles/Dockerfile\", \"r\") as f:\n", + " dockerfile_contents=f.read()\n", + "my_env.docker.base_dockerfile=dockerfile_contents\n", + "my_env.docker.base_image = None\n", + "my_env.python.interpreter_path = '/opt/miniconda/bin/python'\n", + "my_env.python.user_managed_dependencies = True\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Create a ScriptRunConfig\n", + "\n", + "Use the [ScriptRunConfig](https://docs.microsoft.com/python/api/azureml-core/azureml.core.scriptrunconfig?view=azure-ml-py) class to define your run. Specify the source directory, compute target, and environment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from azureml.train.dnn import PyTorch\n", + "from azureml.core import ScriptRunConfig\n", + "\n", + "model_name = 'pytorch-peds'\n", + "output_dir = './outputs/'\n", + "n_epochs = 2\n", + "\n", + "script_args = [\n", + " '--model_name', model_name,\n", + " '--output_dir', output_dir,\n", + " '--n_epochs', n_epochs,\n", + "]\n", + "# Add training script to run config\n", + "runconfig = ScriptRunConfig(\n", + " source_directory=project_folder,\n", + " script=\"script.py\",\n", + " arguments=script_args)\n", + "\n", + "# Attach compute target to run config\n", + "runconfig.run_config.target = cluster_name\n", + "\n", + "# Uncomment the line below if you want to try this locally first\n", + "#runconfig.run_config.target = \"local\"\n", + "\n", + "# Attach environment to run config\n", + "runconfig.run_config.environment = my_env" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Train remotely\n", + "\n", + "### Submit your run" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Submit run \n", + "run = experiment.submit(runconfig)\n", + "\n", + "# to get more details of your run\n", + "print(run.get_details())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Monitor your run\n", + "\n", + "Use a widget to keep track of your run. You can also view the status of the run within the [Azure Machine Learning service portal](https://ml.azure.com)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from azureml.widgets import RunDetails\n", + "\n", + "RunDetails(run).show()\n", + "run.wait_for_completion(show_output=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Test your model\n", + "\n", + "Now that we are done training, let's see how well this model actually performs.\n", + "\n", + "### Get your latest run\n", + "First, pull the latest run using `experiment.get_runs()`, which lists runs from `experiment` in reverse chronological order." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from azureml.core import Run\n", + "\n", + "last_run = next(experiment.get_runs())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Register your model\n", + "Next, [register the model](https://docs.microsoft.com/azure/machine-learning/concept-model-management-and-deployment#register-package-and-deploy-models-from-anywhere) from your run. Registering your model assigns it a version and helps you with auditability." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "last_run.register_model(model_name=model_name, model_path=os.path.join(output_dir, model_name))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Download your model\n", + "Next, download this registered model. Notice how we can initialize the `Model` object with the name of the registered model, rather than a path to the file itself." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from azureml.core import Model\n", + "\n", + "model = Model(workspace=ws, name=model_name)\n", + "path = model.download(target_dir='model', exist_ok=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Use your model to make a prediction\n", + "\n", + "Run inferencing on a single test image and display the results." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import torch\n", + "from azureml.core import Dataset\n", + "from data import PennFudanDataset\n", + "from script import get_transform, download_data, NUM_CLASSES\n", + "from model import get_instance_segmentation_model\n", + "\n", + "if torch.cuda.is_available():\n", + " device = torch.device('cuda')\n", + "else:\n", + " device = torch.device('cpu')\n", + "\n", + "# Instantiate model with correct weights, cast to correct device, place in evaluation mode\n", + "predict_model = get_instance_segmentation_model(NUM_CLASSES)\n", + "predict_model.to(device)\n", + "predict_model.load_state_dict(torch.load(path, map_location=device))\n", + "predict_model.eval()\n", + "\n", + "# Load dataset\n", + "root_dir=download_data()\n", + "dataset_test = PennFudanDataset(root=root_dir, transforms=get_transform(train=False))\n", + "\n", + "# pick one image from the test set\n", + "img, _ = dataset_test[0]\n", + "\n", + "with torch.no_grad():\n", + " prediction = predict_model([img.to(device)])\n", + "\n", + "# model = torch.load(path)\n", + "#torch.load(model.get_model_path(model_name='outputs/model.pt'))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Display the input image\n", + "\n", + "While tensors are great for computers, a tensor of RGB values doesn't mean much to a human. Let's display the input image in a way that a human could understand." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from PIL import Image\n", + "\n", + "\n", + "Image.fromarray(img.mul(255).permute(1, 2, 0).byte().numpy())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Display the predicted masks\n", + "\n", + "The prediction consists of masks, displaying the outline of pedestrians in the image. Let's take a look at the first two masks, below." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Image.fromarray(prediction[0]['masks'][0, 0].mul(255).byte().cpu().numpy())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Image.fromarray(prediction[0]['masks'][1, 0].mul(255).byte().cpu().numpy())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Next steps\n", + "\n", + "Congratulations! You just trained a Mask R-CNN model with PyTorch in Azure Machine Learning. As next steps, consider:\n", + "1. Learn more about using PyTorch in Azure Machine Learning service by checking out the [README](./README.md]\n", + "2. Try exporting your model to [ONNX](https://docs.microsoft.com/azure/machine-learning/concept-onnx) for accelerated inferencing." + ] + } + ], + "metadata": { + "authors": [ + { + "name": "gopalv" + } + ], + "category": "training", + "compute": [ + "AML Compute" + ], + "datasets": [ + "Custom" + ], + "deployment": [ + "None" + ], + "exclude_from_index": false, + "framework": [ + "PyTorch" + ], + "friendly_name": "PyTorch object detection", + "index_order": 1, + "kernel_info": { + "name": "python3" + }, + "kernelspec": { + "display_name": "Python 3.6", + "language": "python", + "name": "python36" + }, + "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-final" + }, + "nteract": { + "version": "nteract-front-end@1.0.0" + }, + "tags": [ + "remote run", + "docker" + ], + "task": "Fine-tune PyTorch object detection model with a custom dockerfile" + }, + "nbformat": 4, + "nbformat_minor": 2 +} \ No newline at end of file diff --git a/how-to-use-azureml/ml-frameworks/pytorch/training/mask-rcnn-object-detection/pytorch-mask-rcnn.yml b/how-to-use-azureml/ml-frameworks/pytorch/training/mask-rcnn-object-detection/pytorch-mask-rcnn.yml new file mode 100644 index 00000000..4302c349 --- /dev/null +++ b/how-to-use-azureml/ml-frameworks/pytorch/training/mask-rcnn-object-detection/pytorch-mask-rcnn.yml @@ -0,0 +1,14 @@ +name: pytorch-mask-rcnn +dependencies: +- cython +- pytorch -c pytorch +- torchvision -c pytorch +- pip: + - azureml-sdk + - azureml-widgets + - azureml-dataprep + - fuse + - pandas + - matplotlib + - pillow==7.0.0 + - git+https://github.com/philferriere/cocoapi.git#subdirectory=PythonAPI diff --git a/how-to-use-azureml/ml-frameworks/pytorch/training/mask-rcnn-object-detection/script.py b/how-to-use-azureml/ml-frameworks/pytorch/training/mask-rcnn-object-detection/script.py new file mode 100644 index 00000000..5851cffa --- /dev/null +++ b/how-to-use-azureml/ml-frameworks/pytorch/training/mask-rcnn-object-detection/script.py @@ -0,0 +1,117 @@ +import argparse +import os +import torch +import torchvision +import transforms as T +import urllib.request +import utils + +from azureml.core import Dataset, Run +from data import PennFudanDataset +from engine import train_one_epoch, evaluate +from model import get_instance_segmentation_model +from zipfile import ZipFile + +NUM_CLASSES = 2 + + +def download_data(): + data_file = 'PennFudanPed.zip' + ds_path = 'PennFudanPed/' + urllib.request.urlretrieve('https://www.cis.upenn.edu/~jshi/ped_html/PennFudanPed.zip', data_file) + zip = ZipFile(file=data_file) + zip.extractall(path=ds_path) + return os.path.join(ds_path, zip.namelist()[0]) + + +def get_transform(train): + transforms = [] + # converts the image, a PIL image, into a PyTorch Tensor + transforms.append(T.ToTensor()) + if train: + # during training, randomly flip the training images + # and ground-truth for data augmentation + transforms.append(T.RandomHorizontalFlip(0.5)) + return T.Compose(transforms) + + +def main(): + print("Torch version:", torch.__version__) + # get command-line arguments + parser = argparse.ArgumentParser() + parser.add_argument('--model_name', type=str, default="pytorch-peds.pt", + help='name with which to register your model') + parser.add_argument('--output_dir', default="local-outputs", + type=str, help='output directory') + parser.add_argument('--n_epochs', type=int, + default=10, help='number of epochs') + args = parser.parse_args() + + # In case user inputs a nested output directory + os.makedirs(name=args.output_dir, exist_ok=True) + + # Get a dataset by name + root_dir = download_data() + + # use our dataset and defined transformations + dataset = PennFudanDataset(root=root_dir, transforms=get_transform(train=True)) + dataset_test = PennFudanDataset(root=root_dir, transforms=get_transform(train=False)) + + # split the dataset in train and test set + torch.manual_seed(1) + indices = torch.randperm(len(dataset)).tolist() + dataset = torch.utils.data.Subset(dataset, indices[:-50]) + dataset_test = torch.utils.data.Subset(dataset_test, indices[-50:]) + + # define training and validation data loaders + data_loader = torch.utils.data.DataLoader( + dataset, batch_size=2, shuffle=True, num_workers=4, + collate_fn=utils.collate_fn) + + data_loader_test = torch.utils.data.DataLoader( + dataset_test, batch_size=1, shuffle=False, num_workers=4, + collate_fn=utils.collate_fn) + + if torch.cuda.is_available(): + print('Using GPU') + device = torch.device('cuda') + else: + print('Using CPU') + device = torch.device('cpu') + + # our dataset has two classes only - background and person + num_classes = NUM_CLASSES + + # get the model using our helper function + model = get_instance_segmentation_model(num_classes) + + # move model to the right device + model.to(device) + + # construct an optimizer + params = [p for p in model.parameters() if p.requires_grad] + optimizer = torch.optim.SGD(params, lr=0.005, + momentum=0.9, weight_decay=0.0005) + + # and a learning rate scheduler which decreases the learning rate by + # 10x every 3 epochs + lr_scheduler = torch.optim.lr_scheduler.StepLR(optimizer, + step_size=3, + gamma=0.1) + + for epoch in range(args.n_epochs): + # train for one epoch, printing every 10 iterations + train_one_epoch( + model, optimizer, data_loader, device, epoch, print_freq=10) + # update the learning rate + lr_scheduler.step() + # evaluate on the test dataset + evaluate(model, data_loader_test, device=device) + + # Saving the state dict is recommended method, per + # https://pytorch.org/tutorials/beginner/saving_loading_models.html + torch.save(model.state_dict(), os.path.join(args.output_dir, args.model_name)) + + +if __name__ == '__main__': + main() diff --git a/how-to-use-azureml/ml-frameworks/pytorch/training/mask-rcnn-object-detection/transforms.py b/how-to-use-azureml/ml-frameworks/pytorch/training/mask-rcnn-object-detection/transforms.py new file mode 100644 index 00000000..73efc92b --- /dev/null +++ b/how-to-use-azureml/ml-frameworks/pytorch/training/mask-rcnn-object-detection/transforms.py @@ -0,0 +1,50 @@ +import random +import torch + +from torchvision.transforms import functional as F + + +def _flip_coco_person_keypoints(kps, width): + flip_inds = [0, 2, 1, 4, 3, 6, 5, 8, 7, 10, 9, 12, 11, 14, 13, 16, 15] + flipped_data = kps[:, flip_inds] + flipped_data[..., 0] = width - flipped_data[..., 0] + # Maintain COCO convention that if visibility == 0, then x, y = 0 + inds = flipped_data[..., 2] == 0 + flipped_data[inds] = 0 + return flipped_data + + +class Compose(object): + def __init__(self, transforms): + self.transforms = transforms + + def __call__(self, image, target): + for t in self.transforms: + image, target = t(image, target) + return image, target + + +class RandomHorizontalFlip(object): + def __init__(self, prob): + self.prob = prob + + def __call__(self, image, target): + if random.random() < self.prob: + height, width = image.shape[-2:] + image = image.flip(-1) + bbox = target["boxes"] + bbox[:, [0, 2]] = width - bbox[:, [2, 0]] + target["boxes"] = bbox + if "masks" in target: + target["masks"] = target["masks"].flip(-1) + if "keypoints" in target: + keypoints = target["keypoints"] + keypoints = _flip_coco_person_keypoints(keypoints, width) + target["keypoints"] = keypoints + return image, target + + +class ToTensor(object): + def __call__(self, image, target): + image = F.to_tensor(image) + return image, target diff --git a/how-to-use-azureml/ml-frameworks/pytorch/training/mask-rcnn-object-detection/utils.py b/how-to-use-azureml/ml-frameworks/pytorch/training/mask-rcnn-object-detection/utils.py new file mode 100644 index 00000000..0e8e8560 --- /dev/null +++ b/how-to-use-azureml/ml-frameworks/pytorch/training/mask-rcnn-object-detection/utils.py @@ -0,0 +1,326 @@ +from __future__ import print_function + +from collections import defaultdict, deque +import datetime +import pickle +import time + +import torch +import torch.distributed as dist + +import errno +import os + + +class SmoothedValue(object): + """Track a series of values and provide access to smoothed values over a + window or the global series average. + """ + + def __init__(self, window_size=20, fmt=None): + if fmt is None: + fmt = "{median:.4f} ({global_avg:.4f})" + self.deque = deque(maxlen=window_size) + self.total = 0.0 + self.count = 0 + self.fmt = fmt + + def update(self, value, n=1): + self.deque.append(value) + self.count += n + self.total += value * n + + def synchronize_between_processes(self): + """ + Warning: does not synchronize the deque! + """ + if not is_dist_avail_and_initialized(): + return + t = torch.tensor([self.count, self.total], dtype=torch.float64, device='cuda') + dist.barrier() + dist.all_reduce(t) + t = t.tolist() + self.count = int(t[0]) + self.total = t[1] + + @property + def median(self): + d = torch.tensor(list(self.deque)) + return d.median().item() + + @property + def avg(self): + d = torch.tensor(list(self.deque), dtype=torch.float32) + return d.mean().item() + + @property + def global_avg(self): + return self.total / self.count + + @property + def max(self): + return max(self.deque) + + @property + def value(self): + return self.deque[-1] + + def __str__(self): + return self.fmt.format( + median=self.median, + avg=self.avg, + global_avg=self.global_avg, + max=self.max, + value=self.value) + + +def all_gather(data): + """ + Run all_gather on arbitrary picklable data (not necessarily tensors) + Args: + data: any picklable object + Returns: + list[data]: list of data gathered from each rank + """ + world_size = get_world_size() + if world_size == 1: + return [data] + + # serialized to a Tensor + buffer = pickle.dumps(data) + storage = torch.ByteStorage.from_buffer(buffer) + tensor = torch.ByteTensor(storage).to("cuda") + + # obtain Tensor size of each rank + local_size = torch.tensor([tensor.numel()], device="cuda") + size_list = [torch.tensor([0], device="cuda") for _ in range(world_size)] + dist.all_gather(size_list, local_size) + size_list = [int(size.item()) for size in size_list] + max_size = max(size_list) + + # receiving Tensor from all ranks + # we pad the tensor because torch all_gather does not support + # gathering tensors of different shapes + tensor_list = [] + for _ in size_list: + tensor_list.append(torch.empty((max_size,), dtype=torch.uint8, device="cuda")) + if local_size != max_size: + padding = torch.empty(size=(max_size - local_size,), dtype=torch.uint8, device="cuda") + tensor = torch.cat((tensor, padding), dim=0) + dist.all_gather(tensor_list, tensor) + + data_list = [] + for size, tensor in zip(size_list, tensor_list): + buffer = tensor.cpu().numpy().tobytes()[:size] + data_list.append(pickle.loads(buffer)) + + return data_list + + +def reduce_dict(input_dict, average=True): + """ + Args: + input_dict (dict): all the values will be reduced + average (bool): whether to do average or sum + Reduce the values in the dictionary from all processes so that all processes + have the averaged results. Returns a dict with the same fields as + input_dict, after reduction. + """ + world_size = get_world_size() + if world_size < 2: + return input_dict + with torch.no_grad(): + names = [] + values = [] + # sort the keys so that they are consistent across processes + for k in sorted(input_dict.keys()): + names.append(k) + values.append(input_dict[k]) + values = torch.stack(values, dim=0) + dist.all_reduce(values) + if average: + values /= world_size + reduced_dict = {k: v for k, v in zip(names, values)} + return reduced_dict + + +class MetricLogger(object): + def __init__(self, delimiter="\t"): + self.meters = defaultdict(SmoothedValue) + self.delimiter = delimiter + + def update(self, **kwargs): + for k, v in kwargs.items(): + if isinstance(v, torch.Tensor): + v = v.item() + assert isinstance(v, (float, int)) + self.meters[k].update(v) + + def __getattr__(self, attr): + if attr in self.meters: + return self.meters[attr] + if attr in self.__dict__: + return self.__dict__[attr] + raise AttributeError("'{}' object has no attribute '{}'".format( + type(self).__name__, attr)) + + def __str__(self): + loss_str = [] + for name, meter in self.meters.items(): + loss_str.append( + "{}: {}".format(name, str(meter)) + ) + return self.delimiter.join(loss_str) + + def synchronize_between_processes(self): + for meter in self.meters.values(): + meter.synchronize_between_processes() + + def add_meter(self, name, meter): + self.meters[name] = meter + + def log_every(self, iterable, print_freq, header=None): + i = 0 + if not header: + header = '' + start_time = time.time() + end = time.time() + iter_time = SmoothedValue(fmt='{avg:.4f}') + data_time = SmoothedValue(fmt='{avg:.4f}') + space_fmt = ':' + str(len(str(len(iterable)))) + 'd' + if torch.cuda.is_available(): + log_msg = self.delimiter.join([ + header, + '[{0' + space_fmt + '}/{1}]', + 'eta: {eta}', + '{meters}', + 'time: {time}', + 'data: {data}', + 'max mem: {memory:.0f}' + ]) + else: + log_msg = self.delimiter.join([ + header, + '[{0' + space_fmt + '}/{1}]', + 'eta: {eta}', + '{meters}', + 'time: {time}', + 'data: {data}' + ]) + MB = 1024.0 * 1024.0 + for obj in iterable: + data_time.update(time.time() - end) + yield obj + iter_time.update(time.time() - end) + if i % print_freq == 0 or i == len(iterable) - 1: + eta_seconds = iter_time.global_avg * (len(iterable) - i) + eta_string = str(datetime.timedelta(seconds=int(eta_seconds))) + if torch.cuda.is_available(): + print(log_msg.format( + i, len(iterable), eta=eta_string, + meters=str(self), + time=str(iter_time), data=str(data_time), + memory=torch.cuda.max_memory_allocated() / MB)) + else: + print(log_msg.format( + i, len(iterable), eta=eta_string, + meters=str(self), + time=str(iter_time), data=str(data_time))) + i += 1 + end = time.time() + total_time = time.time() - start_time + total_time_str = str(datetime.timedelta(seconds=int(total_time))) + print('{} Total time: {} ({:.4f} s / it)'.format( + header, total_time_str, total_time / len(iterable))) + + +def collate_fn(batch): + return tuple(zip(*batch)) + + +def warmup_lr_scheduler(optimizer, warmup_iters, warmup_factor): + + def f(x): + if x >= warmup_iters: + return 1 + alpha = float(x) / warmup_iters + return warmup_factor * (1 - alpha) + alpha + + return torch.optim.lr_scheduler.LambdaLR(optimizer, f) + + +def mkdir(path): + try: + os.makedirs(path) + except OSError as e: + if e.errno != errno.EEXIST: + raise + + +def setup_for_distributed(is_master): + """ + This function disables printing when not in master process + """ + import builtins as __builtin__ + builtin_print = __builtin__.print + + def print(*args, **kwargs): + force = kwargs.pop('force', False) + if is_master or force: + builtin_print(*args, **kwargs) + + __builtin__.print = print + + +def is_dist_avail_and_initialized(): + if not dist.is_available(): + return False + if not dist.is_initialized(): + return False + return True + + +def get_world_size(): + if not is_dist_avail_and_initialized(): + return 1 + return dist.get_world_size() + + +def get_rank(): + if not is_dist_avail_and_initialized(): + return 0 + return dist.get_rank() + + +def is_main_process(): + return get_rank() == 0 + + +def save_on_master(*args, **kwargs): + if is_main_process(): + torch.save(*args, **kwargs) + + +def init_distributed_mode(args): + if 'RANK' in os.environ and 'WORLD_SIZE' in os.environ: + args.rank = int(os.environ["RANK"]) + args.world_size = int(os.environ['WORLD_SIZE']) + args.gpu = int(os.environ['LOCAL_RANK']) + elif 'SLURM_PROCID' in os.environ: + args.rank = int(os.environ['SLURM_PROCID']) + args.gpu = args.rank % torch.cuda.device_count() + else: + print('Not using distributed mode') + args.distributed = False + return + + args.distributed = True + + torch.cuda.set_device(args.gpu) + args.dist_backend = 'nccl' + print('| distributed init (rank {}): {}'.format( + args.rank, args.dist_url), flush=True) + torch.distributed.init_process_group(backend=args.dist_backend, init_method=args.dist_url, + world_size=args.world_size, rank=args.rank) + torch.distributed.barrier() + setup_for_distributed(args.rank == 0) diff --git a/how-to-use-azureml/ml-frameworks/scikit-learn/training/train-hyperparameter-tune-deploy-with-sklearn/train-hyperparameter-tune-deploy-with-sklearn.ipynb b/how-to-use-azureml/ml-frameworks/scikit-learn/training/train-hyperparameter-tune-deploy-with-sklearn/train-hyperparameter-tune-deploy-with-sklearn.ipynb index a501e22f..281864b3 100644 --- a/how-to-use-azureml/ml-frameworks/scikit-learn/training/train-hyperparameter-tune-deploy-with-sklearn/train-hyperparameter-tune-deploy-with-sklearn.ipynb +++ b/how-to-use-azureml/ml-frameworks/scikit-learn/training/train-hyperparameter-tune-deploy-with-sklearn/train-hyperparameter-tune-deploy-with-sklearn.ipynb @@ -487,6 +487,15 @@ "hyperdrive_run.wait_for_completion(show_output=True)" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert(hyperdrive_run.get_status() == \"Completed\")" + ] + }, { "cell_type": "markdown", "metadata": {}, diff --git a/how-to-use-azureml/ml-frameworks/tensorflow/deployment/train-hyperparameter-tune-deploy-with-tensorflow/train-hyperparameter-tune-deploy-with-tensorflow.ipynb b/how-to-use-azureml/ml-frameworks/tensorflow/deployment/train-hyperparameter-tune-deploy-with-tensorflow/train-hyperparameter-tune-deploy-with-tensorflow.ipynb index 1d4f2de7..314161c1 100644 --- a/how-to-use-azureml/ml-frameworks/tensorflow/deployment/train-hyperparameter-tune-deploy-with-tensorflow/train-hyperparameter-tune-deploy-with-tensorflow.ipynb +++ b/how-to-use-azureml/ml-frameworks/tensorflow/deployment/train-hyperparameter-tune-deploy-with-tensorflow/train-hyperparameter-tune-deploy-with-tensorflow.ipynb @@ -255,7 +255,8 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "You may want to regiester datasets using the register() method to your workspace so they can be shared with others, reused across various experiments, and referred to by name in your training script." + "You may want to regiester datasets using the register() method to your workspace so they can be shared with others, reused across various experiments, and referred to by name in your training script.\n", + "You can try get the dataset first to see if it's already registered." ] }, { @@ -264,10 +265,18 @@ "metadata": {}, "outputs": [], "source": [ - "dataset = dataset.register(workspace = ws,\n", - " name = 'mnist dataset',\n", - " description='training and test dataset',\n", - " create_new_version=True)\n", + "dataset_registered = False\n", + "try:\n", + " temp = Dataset.get_by_name(workspace = ws, name = 'mnist-dataset')\n", + " dataset_registered = True\n", + "except:\n", + " print(\"The dataset mnist-dataset is not registered in workspace yet.\")\n", + "\n", + "if not dataset_registered:\n", + " dataset = dataset.register(workspace = ws,\n", + " name = 'mnist-dataset',\n", + " description='training and test dataset',\n", + " create_new_version=True)\n", "# list the files referenced by dataset\n", "dataset.to_path()" ] @@ -823,6 +832,15 @@ "htr.wait_for_completion(show_output=True)" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert(htr.get_status() == \"Completed\")" + ] + }, { "cell_type": "markdown", "metadata": {}, diff --git a/how-to-use-azureml/ml-frameworks/tensorflow/training/hyperparameter-tune-and-warm-start-with-tensorflow/hyperparameter-tune-and-warm-start-with-tensorflow.ipynb b/how-to-use-azureml/ml-frameworks/tensorflow/training/hyperparameter-tune-and-warm-start-with-tensorflow/hyperparameter-tune-and-warm-start-with-tensorflow.ipynb index cddc5d42..aabcacc5 100644 --- a/how-to-use-azureml/ml-frameworks/tensorflow/training/hyperparameter-tune-and-warm-start-with-tensorflow/hyperparameter-tune-and-warm-start-with-tensorflow.ipynb +++ b/how-to-use-azureml/ml-frameworks/tensorflow/training/hyperparameter-tune-and-warm-start-with-tensorflow/hyperparameter-tune-and-warm-start-with-tensorflow.ipynb @@ -255,7 +255,8 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Use the register() method to register datasets to your workspace so they can be shared with others, reused across various experiments, and referred to by name in your training script." + "Use the register() method to register datasets to your workspace so they can be shared with others, reused across various experiments, and referred to by name in your training script.\n", + "You can try get the dataset first to see if it's already registered." ] }, { @@ -264,10 +265,18 @@ "metadata": {}, "outputs": [], "source": [ - "dataset = dataset.register(workspace = ws,\n", - " name = 'mnist dataset',\n", - " description='training and test dataset',\n", - " create_new_version=True)" + "dataset_registered = False\n", + "try:\n", + " temp = Dataset.get_by_name(workspace = ws, name = 'mnist-dataset')\n", + " dataset_registered = True\n", + "except:\n", + " print(\"The dataset mnist-dataset is not registered in workspace yet.\")\n", + "\n", + "if not dataset_registered:\n", + " dataset = dataset.register(workspace = ws,\n", + " name = 'mnist-dataset',\n", + " description='training and test dataset',\n", + " create_new_version=True)" ] }, { @@ -634,6 +643,15 @@ "htr.wait_for_completion(show_output=True)" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert(htr.get_status() == \"Completed\")" + ] + }, { "cell_type": "markdown", "metadata": {}, diff --git a/how-to-use-azureml/ml-frameworks/tensorflow/training/train-tensorflow-resume-training/train-tensorflow-resume-training.ipynb b/how-to-use-azureml/ml-frameworks/tensorflow/training/train-tensorflow-resume-training/train-tensorflow-resume-training.ipynb index be6851fe..71f1e7ec 100644 --- a/how-to-use-azureml/ml-frameworks/tensorflow/training/train-tensorflow-resume-training/train-tensorflow-resume-training.ipynb +++ b/how-to-use-azureml/ml-frameworks/tensorflow/training/train-tensorflow-resume-training/train-tensorflow-resume-training.ipynb @@ -170,7 +170,8 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "you may want to register datasets using the register() method to your workspace so they can be shared with others, reused across various experiments, and referred to by name in your training script." + "you may want to register datasets using the register() method to your workspace so they can be shared with others, reused across various experiments, and referred to by name in your training script.\n", + "You can try get the dataset first to see if it's already registered." ] }, { @@ -179,11 +180,19 @@ "metadata": {}, "outputs": [], "source": [ - "#register dataset to workspace\n", - "dataset = dataset.register(workspace = ws,\n", - " name = 'mnist dataset',\n", - " description='training and test dataset',\n", - " create_new_version=True)" + "dataset_registered = False\n", + "try:\n", + " temp = Dataset.get_by_name(workspace = ws, name = 'mnist-dataset')\n", + " dataset_registered = True\n", + "except:\n", + " print(\"The dataset mnist-dataset is not registered in workspace yet.\")\n", + "\n", + "if not dataset_registered:\n", + " #register dataset to workspace\n", + " dataset = dataset.register(workspace = ws,\n", + " name = 'mnist-dataset',\n", + " description='training and test dataset',\n", + " create_new_version=True)" ] }, { diff --git a/how-to-use-azureml/track-and-monitor-experiments/logging-api/logging-api.ipynb b/how-to-use-azureml/track-and-monitor-experiments/logging-api/logging-api.ipynb index e1a16f21..fbc0d506 100644 --- a/how-to-use-azureml/track-and-monitor-experiments/logging-api/logging-api.ipynb +++ b/how-to-use-azureml/track-and-monitor-experiments/logging-api/logging-api.ipynb @@ -100,7 +100,7 @@ "\n", "# Check core SDK version number\n", "\n", - "print(\"This notebook was created using SDK version 1.2.0, you are currently running version\", azureml.core.VERSION)" + "print(\"This notebook was created using SDK version 1.3.0, you are currently running version\", azureml.core.VERSION)" ] }, { diff --git a/how-to-use-azureml/training-with-deep-learning/train-hyperparameter-tune-deploy-with-keras/train-hyperparameter-tune-deploy-with-keras.ipynb b/how-to-use-azureml/training-with-deep-learning/train-hyperparameter-tune-deploy-with-keras/train-hyperparameter-tune-deploy-with-keras.ipynb index a8e3bdfa..ed1519f2 100644 --- a/how-to-use-azureml/training-with-deep-learning/train-hyperparameter-tune-deploy-with-keras/train-hyperparameter-tune-deploy-with-keras.ipynb +++ b/how-to-use-azureml/training-with-deep-learning/train-hyperparameter-tune-deploy-with-keras/train-hyperparameter-tune-deploy-with-keras.ipynb @@ -243,7 +243,8 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Use the `register()` method to register datasets to your workspace so they can be shared with others, reused across various experiments, and referred to by name in your training script." + "Use the `register()` method to register datasets to your workspace so they can be shared with others, reused across various experiments, and referred to by name in your training script.\n", + "You can try get the dataset first to see if it's already registered." ] }, { @@ -252,10 +253,18 @@ "metadata": {}, "outputs": [], "source": [ - "dataset = dataset.register(workspace = ws,\n", - " name = 'mnist dataset',\n", - " description='training and test dataset',\n", - " create_new_version=True)" + "dataset_registered = False\n", + "try:\n", + " temp = Dataset.get_by_name(workspace = ws, name = 'mnist-dataset')\n", + " dataset_registered = True\n", + "except:\n", + " print(\"The dataset mnist-dataset is not registered in workspace yet.\")\n", + "\n", + "if not dataset_registered:\n", + " dataset = dataset.register(workspace = ws,\n", + " name = 'mnist-dataset',\n", + " description='training and test dataset',\n", + " create_new_version=True)" ] }, { @@ -413,7 +422,7 @@ "metadata": {}, "outputs": [], "source": [ - "dataset = Dataset.get_by_name(ws, 'mnist dataset')\n", + "dataset = Dataset.get_by_name(ws, 'mnist-dataset')\n", "\n", "# list the files referenced by mnist dataset\n", "dataset.to_path()" @@ -801,6 +810,15 @@ "hdr.wait_for_completion(show_output=True)" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert(hdr.get_status() == \"Completed\")" + ] + }, { "cell_type": "markdown", "metadata": {}, diff --git a/how-to-use-azureml/training/train-in-spark/train-in-spark.ipynb b/how-to-use-azureml/training/train-in-spark/train-in-spark.ipynb index 7fc51604..3cacab4b 100644 --- a/how-to-use-azureml/training/train-in-spark/train-in-spark.ipynb +++ b/how-to-use-azureml/training/train-in-spark/train-in-spark.ipynb @@ -286,7 +286,7 @@ "metadata": { "authors": [ { - "name": "aashishb" + "name": "sanpil" } ], "category": "training", diff --git a/index.md b/index.md index 9a180c61..061dfb38 100644 --- a/index.md +++ b/index.md @@ -31,7 +31,7 @@ Machine Learning notebook samples and encourage efficient retrieval of topics an | [Forecasting away from training data](https://github.com/Azure/MachineLearningNotebooks/blob/master//how-to-use-azureml/automated-machine-learning/forecasting-high-frequency/auto-ml-forecasting-function.ipynb) | Forecasting | None | Remote | None | Azure ML AutoML | Forecasting, Confidence Intervals | | [Automated ML run with basic edition features.](https://github.com/Azure/MachineLearningNotebooks/blob/master//how-to-use-azureml/automated-machine-learning/classification-bank-marketing-all-features/auto-ml-classification-bank-marketing-all-features.ipynb) | Classification | Bankmarketing | AML | ACI | None | featurization, explainability, remote_run, AutomatedML | | [Classification of credit card fraudulent transactions using Automated ML](https://github.com/Azure/MachineLearningNotebooks/blob/master//how-to-use-azureml/automated-machine-learning/classification-credit-card-fraud/auto-ml-classification-credit-card-fraud.ipynb) | Classification | Creditcard | AML Compute | None | None | remote_run, AutomatedML | -| [Automated ML run with featurization and model explainability.](https://github.com/Azure/MachineLearningNotebooks/blob/master//how-to-use-azureml/automated-machine-learning/regression-hardware-performance-explanation-and-featurization/auto-ml-regression-hardware-performance-explanation-and-featurization.ipynb) | Regression | MachineData | AML | ACI | None | featurization, explainability, remote_run, AutomatedML | +| [Automated ML run with featurization and model explainability.](https://github.com/Azure/MachineLearningNotebooks/blob/master//how-to-use-azureml/automated-machine-learning/regression-explanation-featurization/auto-ml-regression-explanation-featurization.ipynb) | Regression | MachineData | AML | ACI | None | featurization, explainability, remote_run, AutomatedML | | :star:[Azure Machine Learning Pipeline with DataTranferStep](https://github.com/Azure/MachineLearningNotebooks/blob/master//how-to-use-azureml/machine-learning-pipelines/intro-to-pipelines/aml-pipelines-data-transfer.ipynb) | Demonstrates the use of DataTranferStep | Custom | ADF | None | Azure ML | None | | [Getting Started with Azure Machine Learning Pipelines](https://github.com/Azure/MachineLearningNotebooks/blob/master//how-to-use-azureml/machine-learning-pipelines/intro-to-pipelines/aml-pipelines-getting-started.ipynb) | Getting Started notebook for ANML Pipelines | Custom | AML Compute | None | Azure ML | None | | [Azure Machine Learning Pipeline with AzureBatchStep](https://github.com/Azure/MachineLearningNotebooks/blob/master//how-to-use-azureml/machine-learning-pipelines/intro-to-pipelines/aml-pipelines-how-to-use-azurebatch-to-run-a-windows-executable.ipynb) | Demonstrates the use of AzureBatchStep | Custom | Azure Batch | None | Azure ML | None | @@ -58,6 +58,7 @@ Machine Learning notebook samples and encourage efficient retrieval of topics an | [Training with hyperparameter tuning using PyTorch](https://github.com/Azure/MachineLearningNotebooks/blob/master//how-to-use-azureml/ml-frameworks/pytorch/deployment/train-hyperparameter-tune-deploy-with-pytorch/train-hyperparameter-tune-deploy-with-pytorch.ipynb) | Train an image classification model using transfer learning with the PyTorch estimator | ImageNet | AML Compute | Azure Container Instance | PyTorch | None | | [Distributed PyTorch](https://github.com/Azure/MachineLearningNotebooks/blob/master//how-to-use-azureml/ml-frameworks/pytorch/training/distributed-pytorch-with-horovod/distributed-pytorch-with-horovod.ipynb) | Train a model using the distributed training via Horovod | MNIST | AML Compute | None | PyTorch | None | | [Distributed training with PyTorch](https://github.com/Azure/MachineLearningNotebooks/blob/master//how-to-use-azureml/ml-frameworks/pytorch/training/distributed-pytorch-with-nccl-gloo/distributed-pytorch-with-nccl-gloo.ipynb) | Train a model using distributed training via Nccl/Gloo | MNIST | AML Compute | None | PyTorch | None | +| [PyTorch object detection](https://github.com/Azure/MachineLearningNotebooks/blob/master//how-to-use-azureml/ml-frameworks/pytorch/training/mask-rcnn-object-detection/pytorch-mask-rcnn.ipynb) | Fine-tune PyTorch object detection model with a custom dockerfile | Custom | AML Compute | None | PyTorch | remote run, docker | | [Training and hyperparameter tuning with Scikit-learn](https://github.com/Azure/MachineLearningNotebooks/blob/master//how-to-use-azureml/ml-frameworks/scikit-learn/training/train-hyperparameter-tune-deploy-with-sklearn/train-hyperparameter-tune-deploy-with-sklearn.ipynb) | Train a support vector machine (SVM) to perform classification | Iris | AML Compute | None | Scikit-learn | None | | [Training and hyperparameter tuning using the TensorFlow estimator](https://github.com/Azure/MachineLearningNotebooks/blob/master//how-to-use-azureml/ml-frameworks/tensorflow/deployment/train-hyperparameter-tune-deploy-with-tensorflow/train-hyperparameter-tune-deploy-with-tensorflow.ipynb) | Train a deep neural network | MNIST | AML Compute | Azure Container Instance | TensorFlow | None | | [Distributed training using TensorFlow with Horovod](https://github.com/Azure/MachineLearningNotebooks/blob/master//how-to-use-azureml/ml-frameworks/tensorflow/training/distributed-tensorflow-with-horovod/distributed-tensorflow-with-horovod.ipynb) | Use the TensorFlow estimator to train a word2vec model | None | AML Compute | None | TensorFlow | None | diff --git a/setup-environment/configuration.ipynb b/setup-environment/configuration.ipynb index 7a7312c6..9ce0a661 100644 --- a/setup-environment/configuration.ipynb +++ b/setup-environment/configuration.ipynb @@ -102,7 +102,7 @@ "source": [ "import azureml.core\n", "\n", - "print(\"This notebook was created using version 1.2.0 of the Azure ML SDK\")\n", + "print(\"This notebook was created using version 1.3.0 of the Azure ML SDK\")\n", "print(\"You are currently using version\", azureml.core.VERSION, \"of the Azure ML SDK\")" ] }, diff --git a/tutorials/create-first-ml-experiment/imgs/experiment_main.png b/tutorials/create-first-ml-experiment/imgs/experiment_main.png index bb3e51af7a2143483e9c9e53e5d3c2efa2d0b2ec..2419855bb429e9dc971645dc3817033c5946653b 100644 GIT binary patch literal 69890 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 P|-aPSlTev|QKYbc}9#Aft3fZ&E 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�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 d z#7l%us${n}4ruAhlUZB1b`MI4A0S2>jBD%wK4y%F1-TPFx=C4x4x4Sb_nSiklh#C; z4p*)*(-_M!B1R{=p9j*>w(xO@dYMsBioL0?uP-zl$~V7ra0rxu?Aus9jeK&Y`6qjv z5+6-8PBG5od}8SBkKEi(e@6uoa-y@cvOrQgn(*{Hq03COS*YCyB^qFhWcM7k@`2nA z1pF=@n88>4de5ltvb_AtNk|cQ%_FGW$DhwQU+LyIm+?gI2=#t!J8SGsC<%XM>vAyS zy!kbf##;9K%Xh5pa__Y+lRnSq7`Mxmiy}tGC7m^X8`t~pUbvMG{}-$?cl)5T){OV^ zXDMe}eewYF3bVL-dKkP53&fdzSk$inJPk8hVHvEEQds zPKu{FDZbKu0YPsUE9n6N zSXZabssmbxG&QLgt0EfKtL1oa;>ZfAf1D@*N1{e5YcuWR>eZnkSpJDC$n4;x7I5f* z6AMgeY@(3%wB4N@K!a5AMMOlD66C>Bk%=h+lRtXkW3h4`k{4ey%vzt;tCOYPURq}6 z2yt*0a}cWHhs@tVHnARH+&5HazH%QLo^B!%vJ>_MjaGTTKZ8P2)doE zfl|rPJ>PYF8b%4hBegf&!`^L05l^GE(CGN0R1dXnp& znww%`8Q??=*?}DwDtPBE^5QZzbY~&=lz4aRM--3YChTZ`y0u zaCtFi6{6|Upu7(QmOk0o1oZF$*Jy6zaCh&Ww3Z_p@8-dFz}QB+Si9&)PCa$qB)E4d zbpw_iyYB9x(g-hHm5IGx3KkpgP)0xp$ zV({~B^4jYyRn@~z^$SMBis~!~u>uR2>&BGE;5S37QYO`P(q>S_NKKXhfMPMW!{>K;%ZqS1{`ONDGo=uH7emkC`@E* zqlAF8=r>x-3&A4mFW16feI>qaoR-aX9+2dB$6JhbmLF)0bE~-VCV46Mw>arRBfEa* z_tWqC`*MI)UPw4TA$QJ5qWs7R25fQKt>|8=wgP4qm~qDiI@i`2)JtzKP@e&5!t^ab zV8&^<+Ny*_d**E&47@9R3RfBa4vjpnONQJrikZ&y`$ zg5O$4ohtera)X09A+nhgGIA+ZB`OJ+kh%>@tKE&W?2OLI&9y(yuN>jw=H6)uz5$(O zjz7tpGj&9{P>~t;~7fGqyAw^skA@k$=D3WxbuZ;Bsj`fpDVX3_}?Z3{t1kycKfWC}bdoXxYxD zwSiW%^umJn^HPuHWa+IL+fGqZ<)Y*Yyu+2`*vXWa^$`2BC)#V_gfMXR$K0dzZ%=KB zd%j<(rY=2Yg0Vobvv#xYZOw)wuF;99P`i4;KAbbbV;`h{z4yPBTFc2<8jot_Fq}hX zpG1)C#$LVfu<$&O=XcCbVJ()_Cr6WZZDC)R4k`7Kb$)C>J0U;PlsZr-X4ZU~J0H#d znbtV0;dXsu)J5Oe_~Zw>`A>amn#=fi(9YZ;PIT$ujhQoCaCW+B$+3|6P>QxYqWkrH z1lxq4BpEa1)W}%x@UpW}=BjOQVQRub@qIG7j8sR16F@8h5Z1^jz)SqZ($ExoN zylMEfw}Q@wo$|Kmy+7Tzd1K-HB%I~s7G4~K#G(Z@^4x?Sg3)&&Y*$6fY#K?!8#hjU zF9}$zdqb;Bt9Vw*j=g`l?Al$zi)F!g;RJlL%SqexFROyct{xu2<6kuy1O-VO&ta_d zJ=!eLElG=s!4qExs&l)=9VAy{JG6gd*|nK>c6QEx@fCvAWgrtmOjw52j`BkSw<#?R z%zCYFDYvO5_rHb{?fMVlLn$sFS4eh}4lfI`6R{6&fqEz(tA=iWUlZ!sQJy05-vLrU z?<0A2?(xOxgDxYehv19A_vl+=RwUXjW@gz#zzf_omeU-5TU%QTN`Z_XKR9)N93fEy zQzF~8k^*-d#NH^{56Nc;F6(*x@^(h!C-NDF zhQ{nC-Y=vU9IJ!rcsP`#q-YpnTln+#=x6$}G{y*!cHc7X}VaJ9!zI z1^K|h0DQBkdOFv$rLjq;Ud}m9lx70MX`Su*so6d=L(LaDyRX6{jCoR@=`=J3&T8o8 zSfhc8U6OzUX)b0XV*;0PYvK`n{b5x1k~dRv7cs%>vuFNeN?4#3!O{UOAlEmZ&-xb1 z@L$#xZ#;teuycO(lEDxQk#?p9ha=_8&`Xs3tUe#l`D6cy=VQ`ZS8Sh1Uk@V5vAv`C zAXHP~7Iu$f|79y|w6@AKw4t$lm&*C#ji z7o?IbR>H=rAB2WI{{9)gy+w0p&rf3#+uEIt5MNZCr zYp6n{L46Sq6B26pw&$9AjJj&>kjXndmypc;WIWG2h9;EqBkIi0KK1P<$(K}<9i(!r zzc{`IBx8ABB5GaV1_{mwk!<4awrGpE* z-AUdn-)=~Hd^W**Tl>rW!PeSQyv;cpNxxZavuJ!Rlc3z($3l^*4$*?6U=DuOxZyr4pD9^yFuauDR#Sv&lwP@%A@sPl4eL2E zt_!pWkB*Kwq~dPSI&om*{oL6p`CX3B?&^y#1Wia;pU00M!{Z*xJGo9Q%eV&7f4A(K zrv;p6$iHXi19PJ#g&{aEkEJkQzk=C56wQ9Rn)B>J9ljmc6Su_=;mMsEPX~XRA##Yi zCV0gnTEHtmQ=3(XpC{ z4jp^%wLrSG9M3cYA}<@aMP#vv#XHC%N&TLdVz-gS=UC3@F9-y#w_y8u(>3v!h=19^ zfVLqO>MyBU{#qxl%O8Ahi(g?5ABQq)lu9w`2@|nRs3pK&q-8`n6|&wZMA8{_DDwDC1`~ z)KsqWmvN`g@n3TL{zv{LZ&TK|qWGKLJ?v`kdwIkOCbwC&??DT&e>$CwP0j70%gYm` z(`2GaqveZxnf!lF?Q~wAydppMoH$UOd$o?lwQsr%zpXcK-WV-cz3E*6VfC#-j?sI% zu;Tzgvv3_odo5+CET(x^!rn+va0baN@b?kWmoRwQt5-3VRbdC2Z8R%=eEgb z`42)f35aTP=zH8ROWA1*zD~U_;n&F9Y+^3{?G)8kiry5i%A4-)8aSLgX=b~DfX&C0#Im&EZQ zCJ5-sf4A^1v7u~fc30uu&$ZG`CATsn?UE>KBeMRGY@&j`zfwu#mOjX3_Uv-vk z4AvK*MpW#NtGZ^-z7CsXkF6B6UDvK$i#YM=6T_EK$Z>~+2T+y4(Z@wcC+D>a)8kkz zes`NGk~T!v?e|aZ2NYbF46i+ow`X&>d~OjwgsVi*d8Vt~6#4NJIE+B!G@8MOu+6;R zf%1JED8zQ^l^%kw2vSmsTqv$(N0d2JzE&YXNzhPHt6b)vpX~lHP;5m~OyFwYvJ>Q` zqf#744Tpr!-Mc=koW&t^b#+S}ujB&l5HQF2c<*7xT0ynT=rg+i<*BWi86DnVl#S<||hxkSp;qjJ2KD_VWRm2Q{bw(1i{>cv*V ztMJ<=K8im(=!+k*vl~HN(>j-SQ9qMnk7A!i@tH+}u2-U7?N9QW4Wu`F&&Wc5d~)Pa zKK)U{NIC4C(yr0V~ZdFTSxFB@LXSU!yxtwH5**ez!F*LvJf#bq0>#4?S z)TjOUXolqPZWoC#`E+M0q3Bf;N^3$tVU3}Ex4)X-)zJO`c8UGS!`!HgxO;<7f7L(4WD8$(MAo`h^}1-eQ}^~pVAxm1Z{I!Ao{fdp`VDvW zw6!FzjJMAldpV@(?nhvrNH1~-C$<<5?efn0eQWOJ(koq9AI^p-gE*iH0Fbk=74-_| za77vUW}VmGVgi|jOBlOu@pbqo_jR|4xOya`l$1Zj86bNf9ucwlHiDjk0cjB}%L7V- zU=WFU@82T)HZnrWX}}NtCshQm+E+O+Z!?Kg)q<)Af^ddCv2f<(hhd=8|!arn*J z4$|f`DYp!I?JqxsaSZSx5cOzg`jQ{wCwUVSBS%ID=D)0a+d*hy;}9fteZ$a9kS6L` z2#+h6{+8eJLl}YXt}aF(D>KCYX~&jfh`czu&d(j z+i7@2dJciQxhS?5uES#GBFAc!PZWa?#8A4sbuX#oA;D*3UwmobAe|f08~3y@`=~%b z{KZ^!N6cHy$($=Rg?IKl?>Ap)5PFA~_k3n(kz+xQ>l8zhS@dxgmC8PF!6m_p> z4jzq01wkxnLoFtDeMCw7jlVV#=J6D{MB^}+Dle6A9iwFe4@~%m+BKI_;I$n2!hV>M zcV*|Y$4UPti{}P6B4!I~g>TC|!N}wpuV8Bb@HQcZBR$fPp2Lm0k2e?XITp4#aoqw5 z7ah$sskox`>2+Wb4j$VD4si3AT5)?`+lZ*!|Cx=b2Q6m!Jd|nJ zo|Z)gpY?Tjw;bRivIYkQA)Rc(N5RI*^=FDLWP7)uBs0ecPQ-~itdGeCwWqd#Yg1^5%N(RPt80t3uOT@Cr zFnQgpg{Ss9Ku&nka^TPcyu^bSgh2>F`X4md4)r1ML#L@mHa25!Zr+uF<@~~t5|&_* zjhmH*@SMN{8eiJd$Y`{P@PuVcGu5tyf`&D7`c^v{@*tMi4aY% zx>jeh&v{;P$Mn7){10qI#Pr$vdi1vsig`qzv)nAMl)3vm;k+qOqu|fc;2}Hgj|=$> zVTgh0!7Vv*r37{@Z;k60a@btd$_ymc2$C2M+<8oVggx%?zgk^J$0MzaCgJQq8QyR& zP_kwrB*%(!>^mJ}koiZs&%wfuE>F+K)Hc?BC4}Q#cAJHGE`oD&t~uhax>i>`v9Krh z%1Qqtj$t#bz=luPX>aZ!F-ki5Aj#_6B^7Sv8;;u{_fJ7VoPUD~Qo7jXtN2ls z)AYhNQ<|9Si6ZrA^V^1uAOF(%WAA=>LJ6aRlX$|nnme|lYLyJG>-Aosj4EnMl45*X z>omu&!*ap)%7qLw&%11_v0u=xo2zxpuBsBKd;qMzgHAP;AtvCG+z}MS1^ywxp{NV> zI8+`y0G?8SSiK$f;liw!E;8aUZPrpgKqULT?c<=BW zBptopR!Y}t@Nl5y1xEK|1+=iQxrja1-vgevw6wi*-%DWR;u?V9X&?mqfkKzcWMTI{ zHPut-5duH!CZ5xq_;|Ebdk2T!5kP5W2RoNGanOJ_x>Nifoj_HbCaP zo~Ypj=Rtp&sL1p>>SWprPM$1ndvk@hYwjdP*ng7VHomnb+gX2AIC*%R zi)Ow#lhf%lwQ~8bh(Xd@93mfeWHFNVsS9|mjLT)MM4XRpZ|GSC|)8>QZ7ibp{im1yD`!`#G@@e+6K?g-YHjv zEI3);o8zqOtGnF+n3Qb>?qZ~9hFx4=uC1gH z*+NyzqK3VY6{r-QN# z`?=7&Ee~_f>@EVOZNGE^+;z@W8_WxPt?Bc)9G<)UKqwT$^f|vco_D>1Kz2YVmADs= zJlwpJurM?4g$X+S*S3ESHxj$y0cE=p1#M*k zmpN(%Heq2eNInh`xlRvJ2nq_aeuA(x{zC|oG=E|B1_Sbe$n$ey`3{I6u_z~xHu?MO zFly8iDq@U+ahTpbnAtI1*BwO+@TNL0&tIBN0&*rRkFyV%c1{dD^0jjRbo&TLF3$-go=iSOU{FU z+0ok@X&rzdI}ISS)~vfN+8-~?B_{AED^s3e$1N={A3~&zF6!P(=w_p$mMQMg+u1^J zyB<9B8kc#(dZYLau>66PS%%xfE3KN)BscfgB4Aqbnbg%^zG1py9cj+nNc_m>C@6l6 zWK>J%t!uP~kmyaw2{*i#Z+gSKyzj>L)zB-UeU%k4M0fh;7>~9z+zw`>Dh^gInxnv! zP~9}LO|M;tV{3nF8H^ROLbEKW6ucaj79DMdaK;+$fv<2D{_pAerR;?h8qV*&;`Pk~ zf2;Vhy6U^+;Z|{urQJU2)G@EQrB-&rAsSckj73yJb#ENUZRnd*+H_gSyICb8$6EWs z_@!#m2!~9W0g1Tsh}G@hE_SsJn=Td?RYQs9yZIu7BZMhjS2n%|zr6RpH+mDsas`hz z=lv@G3ZEeB;v?mYb|D7+>T~WY)^q3k$!g8LdMhc7J;xQvq|bC&*tPG)ytVpB>Q^bI z`l-l61=ZNKOfXs{mf|B#&9VNwnX+iq+X|-IfrEU1p08Cn(O1s*y)gY28!NMpB-(nr zmFa_C;{G=GuB@eo?jIY)xZv>*`C9kdJ&7tmnYZYqkYD^E>Dy-hJXv8}T`Ayo{`l7z zr}%o+TqV>^yz_7DlS^jC^ zuV)%x0y)KC5W^h`q@L?L-3ECY)Pp;8KRq_9zjognYBhaP8v8E8KcN5Sn!?rcrE$yJ z!HMb_o=Zeb_$q3`;z*U2I|(L=_&Nre1L7@95iu40vEw_zH|{py`x=_jZK`|cG7s~6 zFV?eE<%yzO1~Z#c5}U8gHAvfEF@3J%;9U{X!`Lk&!l+#GT+aOvpHQH)!Wwt`#Fum| zro$t|BQ<12bYe6#0vdCYvI`zgH7O$GEtR>1 zWrcTW>=u)cbL<b?hMDSeMUay;AECLjq8aeti!puxQ6^VunhroCuhU}JK*{ux*1I?Q4cTW zzI|BzW>Kc+>ywz2w6VIXGn4Td34jrBB*7c2jqY-{4G%wg*Y$PgxWKy8az+8D!y&VY zi00j0T@Nag8a`%%?#GHLYYzeysCYROqZ*z6%Nie;fpUjDlKVVclUHY%WnUdEg6SBsDuqcmsSy@Veahm!|-_PLT zmGo^k!;9w{C90o3_MNy^Vr z$dN?-g&6jLf5hx=)Im(G&JT9iMd#(MRJ*#c1B)O~VsBT!dO z=UV1#-GWg|hdTeLHb}HdtGn)|ri}o};{oQ0lvbyGB>)qutDPZZlaQ^MC+higVX{z- zwUWiouHxwXhQ<$gEDsTprMW}>esol8i}sxkC8Q=Lymp(%#}n?F#)8DsN(@p?eN$p^ zm)NsE{QM}bn`EN^kdXT#b$PM`$%Kh9N@OmIdik@CZpfG^UeGZER$>Gb8D&;~Kcx_Z z5;SNLER$NECz}(Pm) z$1PCb97h@rOivl9Os~!oo17m?o;?`7u*@OVrbHGTBmL4=4UYZ z(ZJh0<#g{;QKZF)b?455XqDT8#%H?(ln3$D1LtD0@Rt1~lJo`iV+||J--2?(kI~~? zo_-iFF^lI^cNC7T$j^(dvzfh;(>>$XxrJ+IkDPE5Ci>#C!Nwws8QgBKQcV()WA2k9jW3!#x{N z_hzZ|R+rgpjJsw#`zxmu3}0*Tvn&A<)K z`SM;#*{fX}1;{-xBx`(n?>aBw7TdG>+ix`MdJ=n02g$);u@(g%Ku7M%fdFFk#Ye!8 zwqfsGfpNo803z~B9ICO(N1C>_w)qi>iHRWbwC+Hwj=ZQwRqBFYLV{-QF;s|WB)AbP zVK|D+z+U+LWBTz0|H)z`x0*XZxZytxvEDbIVAul|AwV6$oVsQltFeueCl6OU8A3=n zEJ`LAt**BAaC`RbY^N!7{DmN(w1b+~6#SfEd)w9J*$!05T;2Ma?}OSGLoI@|MCSuE zPCMMNm}|7S^X$~@tnpdvj-vbCVmqi&N@?Orkq!anhx_{m4No{Dvqs=-1-@h_1Zx6X z%wd~RDF{0wU%#T@1Z=mUn28|D(=($L4Ifp_BT=U9Wp%Hs_+r_eGW%@ib?@PL#|RD3crseFJm0!cHf~jczSh70?GUycmA}@sV-z`gVSOwDw3Ffr!P45=IDi<$=a~pGX!mxlh8S2WXtA;|DZBdn zo&LpWo6>|dFX>B2=2tcl+oix$|Ah)f5nt2N&;VV5uS;Ny4UJk{G)P=cWJtp`)OC6{ zC?EH3OBE~|VEk8`ty7a^B=q7E{Yk(J*|H((&Ex6%swP~!sO)UU!}#q$tWAVF*YWzW zfQ1FK3B3MLw4x3=}LCPKr?j+*oK^{q%q)fbEljxOD8xI0dH!gcG`t%bje zaYsQSnfC#l!qW&lAWpWiZ%R^|>cvO9*#?e(UHJ8qDWHn9BQoz;0{UfM*?q|ZSX?;H zDUtpsO7DfsU@QS&U~Tb=Hb+AQLykK}{}hQUhAS zDC{q9Z;=Vkp9R^H&LUuA9+rG0y0+Q&0%fjxME_!j%9s7KD&2p}Tw9cimd)%M9Ax?T zf88rL6r{kqI+6dV&d1hZ6!CfhyFctfyxZQ|-(>FAyXcGk9&mNQsK~WH)wQw@lKjIQ z4K5Y@ufEdIuqN}l|JiN$o0-AKj2%Wld1Tt-6OlU-sx8J|Ot{8m6(g1D=D##+9d1f` z99!F;p*FY`+`IR52}{b592~Y0Z?EOk&>1-+qYTO#k$xwu4`4&NJdNXjHN_P+PI~rx zj6dG%F#f|;Nw<#8PmBPWjo*rIzm;~mXEXR?_5ws!H`Ug13;q1w@jW_jgmzyQxyYyr-2;O!GX{ z;VM{Of5F1j(yaMW!w88H)o<_j91eUppG4;${*Il!5_jMeB`N0$`R%sxgM;fkJ~tn0 zs7u%{rB`m`q=>zaO^LmPLhrkWqY2K=OH&#oZfm1qSw0>r4IYo!I!TfHk0_t>5BUD; zkWUDq&@V16HJz@eL{kFhIHt^O1H$=WqM1U*t0Rh{O+l?d>l&quiD^IIcma*ns`wYE zuv8#odf3rQTJRc-2bx4=#9^PI5f;#DI$u$RA`K%D7u#Az5i@rK6Q<6K;PU)n5J=p_ zt?jR(%FD|QYMj5o)&Q={I8ect<)3q1MzcB-hrMl4ngF(^tE(&AYzEbi-5q+-J= z$+78QbBhkFB1k;*?(H-cZkJT5QKaUo_@ykA2|V34^17x%|Io+F^SZixCM>e;Ucd_i z77R7+Zy2m>Y(SCalQO;F@uql9bm|7nBbYM!n4zI(H#C2TB|p zP;?nBfL}V@>x6_*g%}#Km#+5qx9dqsNJ4g@&v7q?_LX^ShU47lq{$mLFK)f<9<4xK zM6O4nfR!h%4Rv6aLB8S#H<|x^i2E&uNbi}s=jP&wUfGNy zI1!Ug=2TZXote`!`?^G-{rJxCvVc$AEkk!7t66>;^+cj=MNMtvAjC_Ro*izWl3V1P_IFr6`BX3~nGksbbX5(MV z-XDB*GJ7Ih>AHOxCFT|qmL)oY;-@hjHMnM;OHVyBRPJV7h3Le20#&H`;%_pXsJz-n zxt}#@uwJN#)nwn%!fH)S!i^6i_~c}M)6XweL~}lxQ~HbD(RUW3N9{Nfw5ByR2ku&U z4E6;p)*bf4)+=S3gZ2h-HM(o3kbJ!QX^1waQ`0m0Vr?S>K90?S=5h zpa$0Kzc_DT%3@@(lG4%?6R_>BbKsqGr4@qI(Qy!$gMf~7@__&0Q^w3n=>(v>p}3hn zdgKjDl(@ckp({tk#DsFa5Wu%NV;TyxqoS<>Cqo1{M7`YsOn@!M;x> zYLY@CB_+L1G?AdU1?Y+T`uZ}?z^wKd)`3w3BW+ngeeWy$E#zWbIPjSuQ46UZy!65Q z0N<3er@Llv)<5}I_U7&}dd3~lSI`&c;8~p!ll;rx>c#rqJ+iT;HDoBH0m?Ek91hvMgD!FZxx7MHR&6&qKJeR__Z)Ha^i6?YFW z)+${kCIrF4`|x#-mekFD%K1ywm?%6fjy<`O<(8PV`oV{!b*(_Fe7JObwJ656t)#7` z;t+ZK>y{)T%@@ujisZ7LLh^dVFf6TaPx5++6+Gxmj(a|2Q_`@-QdzID`;Ufog@(`k z5*u6fIv;wJn(`i=>>Uw`1B47vB|63>-lcy2+yA_j`8Ow-M|RJ@ed1Ow%kC2 zTX5QoE6>G}{e??AiIy8#a?e*Dc!O4XGUW7n_E*PybRZH z*`cD2Ba|lDqbPT$bjxM}gU?IQR*TB;#Sa1~*VYu9m;F$}wP`0&=5(R(V*c2a)H8fH zQ>_o^eDJg!8yg#9fN8joNIF$I!A!*3(Gdh*NtZcsVXZQ~_pm?i_ZRsrK{`J{{-J88 z>5m^jLO)vg`MH;uopJ&7kP;+DBcr2VVnIstdOy`d#oqc?QwYY*gox`F8zu3WBLQtf z<26MM-5#hu@Z6T?d`**Ypo{$ubBHv5U~mvG#+_A!OPkRL6qpz|M_c}xIVY#*`k$C{ z<|d@p(!s5lL!^3Ea(Pozg?U7~dwYRnR_2_}&dzCRX-3OzXog;J&O<-NE~c4IpXVNV zKa^1)X+FRB4UN7$*eb`I$s(y@@X(|8j@Ke{aIE zhVC+B)id5VJOm#z^J-Z?X)n{}9f7`$yaB6(O=$KaPS1(@T?%Y zWy_x27Zq|QZ_znLTJ^b)Qk$1*r1jpd<=>ASQAM<$Keqa^V`-p6<}L9Z#dYf!T7%#B zu_xC4Yw_RZf2kJjaAL-V!gLwKd|#-%xSUBLilzZm4RwlJm-5vqqo=ihLU-z> z?fIa&8Q(8c)qJwBPlb^@WBJy=-i8|pCtBF&cq@G);#ojB0s{W(LwOmkt0I?zhG&Ft z7z~eebxxM|=TWA9@cs7iy_MBJXPE^PD0MykWp4JDG((w1$Zl{`QaopfIqQ2eTR=Vj z<_%54S3G;0w(3q=W)fMqG@sWlk2=yTC7gQFe|E6% zkP~b|Y_zytBJdJEnD?H(hx_qq|8v^7XBzQ-Bh{7pT1f$&Sbx~Uapnz?RXx0vtFtpY z=l!p6j+k2dehcmjJ&k4HOPS+R#eeJUVY*|Q|4))V zLqF%F(uy~#&JSJ{S0GPUatA7K$v!poq`d}!cL+BPHCtjiteeKz;yBtIaO*1Uh<%YP z3(`xvlnT|E&D1S_DyYb^A0HKegJ!)#SRL`#2ps12SS5Yb}?;ZKd$#_lW zm&U-iQhU%9Z<=qf^WE>EDhv9av1y%)cuFj8NhW`g_|H8Ljo%N4;c9$qr{v_SrdD1} zI-2hsVKZI`FGEoB1%i!u2CKSAZWD`@76?RU^#f}i#xtqMo{4|qM#&z_Tt?vj(l+}i z9m0An&?|w51vD_(M5pg|^Iq+QuSud0q4`|K$HB=;o%88C!X(O%9ODbX7x2D~jcsdd z)2Fq1*%cr**x&#A;K0(GnubQ7PXX(JbXXch+UT%PwPC6{@p^*M{H6k7rYT0a>5JV;C_QguQ6Kga*PwgbJw{?!fKMKrR%^EoOykR@2` zkw(Z_+SPHu8kemtcAcg5!2OBA)akkWC8y-LoPc__3X;(yjhzC78;Xcl7Vgo07aQ-t zcUI&*clLLxPH3SKxN>hr3i;!xo^Xay&hm(No%2P1$md}vCwk$D;i$5ONwei%EmB#} zQ80eYn3_8c4&P6e@~kLhSBiO?BB3sryAY;a_9-&iutDWT8oQ-M%znE)Po3!iQTdbl zFc~{x*}l=u#eKW7r&#_34R>qeyND-&!y>-<%63OvG4zGfDe(9J18aWcP42_HEgbgS zE+*rhn^CJ7rP9SeYed=D44*r?i)=aEVMbzvPa~4#&V*BV?5hBQ=5B={spi!fkDm#$ zU|?3|KB*zmOWw~IA*JmDj)IGGXF}d_i^6 zw}ybEaDf3B4%Bu;eb8u)%&+iV$`emRq30*}zF^t*jw| zc$KK1h-}aES)51ZSA{3B3ij_N%c|S2*`Fu^L#B^qho{0(WG%CkOxw34x2B<& zqaD_e2=R#7&x|nhAkKUCy<;IG!^(*>pCF#Pp37PzLqapjXR{&xc=A5Or!o1Az>>+~ zX``*5^DUE9zcxMyDgSTSi=6)>_Tr{mIO3b9e-0nnp}&~i>F#~U9_n*##K3;n(oX_t zM!b*1k6y(>GA{m)jFN%^<_8iq^!kcrA#EalSUT%L`q|YS98B0nyTIijEu!YQ8$DzT znC3-|c>%QhhlEhRU%Tf-+4SgdFC_a-(_F?MSd()u_alw7o3fd+^^8_+rZAl zvaa~!b=2*9s$Nk34|FH%{ld%CI;Bj><$4NDI|ur8jlun%*q{Qjk@OGU$EHMs5XPUL z+=*#*R^(7RP1g~uxwDqH%9^{SqS~#!*>L-kN&Hu5`&{2 z1G4mD_a^HIj6a7o1l9McYHNWvMX&DE zU$t#xj|M$S`~aii?6LaPjXb(&2(W0j(fb{=^GAK z!M?fs4`((7gjtO?R3G+g`ZbBs;E0{6^-vNeu!Z`xF^S{kZVjxThHG1=ZjbNs(A3eB zbkq^kr~1=*{Eb)&lcZ%h#!aG|H|J|yOzn`wBSWz%Aa|qD=;ysXpwZgjk7Gil8V*at zPxc`k1{;&}fXO2%DY=^gHAT|Mc65+9m~PO&O-sYI?HJJ6je>~TwYEBO4|nY1wSw!Q z>xA7Y5RhYIW8t}E2-o^@V6Kel6kg}X%1Q{6z>J9B@^v2mDuZ50NlA^y`9--5t$YHf zA@iFD;Gc#uXX1}W=fdle{Fi%JAy7Hx4*ftw^O=57a?ja$8&nu@UUQ$_CBu23r1WCF zz_dxS{$*|!6KrAtqbI1;4oZiIlaPR)hEaPaDbD02z(<{@Dj%xGHDAanwC<1d)2TtS z^6As3iAWX%%m%O{1j5ri)M~~os%i4``YLAj_7QA|{{$FX({pGLR+cE`FS%-{r$>H^ zt{xJnY<7#Vn#w#43@_x7*g$i4PtWhZj4b^8#Fk%8!pwm&a|M1s>&@&Kq&s?cR6d~_ zv;R3Slyd(5y31D4%8Pi&g_;*Gh`Dc!FhxCrT=Z?-;;8#5tG5!Ce)D+kn;_X?q>;NA z9X6LWla!4c^ecG@NGKEWuAEHIn4*-tkK@F1Hk+K8Z~fLXG2Z^7ldoKGX?)N7=yj0P zkl|vRP_sjIp|d%=kMFdW3nCFBC?1i$ujQzfPG=e&B(`NR#^<&y6}@ykG%vH#R7wd6 z=_MCv4zS*BKrjyQFc$Nsx+ z5lmmKmPHd4&u6v!^m%^9FNRz>iz4ZW6PxpOR3-}RSsCTIx ztVN#=q5VY_9i5*y{bfzBvvBnPz=}HHchQK6i7f{*er+QtL`}7a?b?NM>aQpTf`sm( z0S-e;9X{iuxHzK_DmXgJhOf}rzvGHRN?Ogg-tvZ+G`5Tjut8xdP8%-11iov4wX}^_ zC!t{TV?OD6SbzGj=Bb(i5BgEHv@?$y=Rd7eX*8{<_A z6aT%MU}@+mdR=!E6QBRT=qMtZN_0v3v4Mp2p=cSMQ*D>rl*3?mmO|1`xUrPuOC_yl z3A2xv&pZ)uIE66B{9PZpPJ<3^3!A&(KmvzXlI-f3^<58$oUm2n?inv^B4o-w%WhBj z0nr8Ee_j>z6T+XxBlr)xiE-y0zi!OS5f+EckM5zwyNXG&qy(j{M0#Zakhz&zWsU{GD0|c_bgnVWSw*JJp0*uKYRc7 zZ=YLE4mJ{E@?t_lLK1ei$DM_QMAU_Zgl#vA0N*^hCr$wV+Ys(-VG(|C|L`&65`kLjf%_--f|7!o!Wx2}- zwfc6n{A9K9r=Kq#$-2Aw#@*t?pr34eMeGoTG&-uv#hbsti{`@}quse2dK@|qH3ORV z_U2NdH4Bh9Z?}XvK8Ixd`|i#E_?HsZv%9sBCkzbz+ktbC0Ve$Yu&{9||8Xv)WSP0~ z$A^fYtOXMKcHp-Cm))?f!uBBj3FaD{ZjgWUi&^UtNY^^PgX8cv?)9f>SZ!AFr0*DP z`Kv=$=+HoiDt&tWPUpolEB6a24b|+)Qn2Y}`U;indpL%-^E|YfvVm(e6%*btahGEl)zOHpAt6Il6+m zS+I_4IxCu2LeG|g&o_LLgd7wd+$e&Bir9#G?tUX#$J#J?CKfdPNSPu2yYJ-3xBc=8 zLbSw-p~hKDpKkRqCr+%ra}m<7cb8EY>F(NS<=oZ_YGpsDvrhdL&mY9GVO^acpLkB* zkS~QbY@}f}2!8Tiy|1;H|CIx`Rjr5aRogx;#8>$VN6QMsOy^m#U+8#GUv=jhNgg|r zDHfd+i7vH0_<2lY_e;f+D%&A>PqW$P@!8vJ?P`GD0!M0ng>`C>=%iFS#eKpqal=DI zqYl&&wYt7mYZ^b7chI!D%o1_=z;1DoGqCi<_a*SSueT|diI<%&oLp&xd#_p?yU$0g zug(~8_O87m4crd1TnD@oU~6+6o`;AlFIZt4fUeu{1S+^9Itq`2_&^_8mfZkp$0`U11| zF)Sb(5qa&(@{!{i-Ul_o9A|?bXD+K@XEt{kI+xF6FRUKPesR(`0C`d}i*_Bmrsg3! z40+gRH%G?@#$4z|<0nl~C{K`BS1~oy8_wn}2Zr#WXqzLbn2C>w;6>#)?-jcJMDUBr z4V0Mn5o*9JdW00b5SjF@^4BIU)BZwk!&-GfvL|s0dk+dDt{LfT&uDe>Zqiy;u3Il1 zaVkIeP5(tI@jKLGs|>^g7MIU~TEL%QL_=K?enA_*T}6_7@}F(D4)9ou=`H{Nm_LIEHGvg+6Uyx-?=%f9} zd=poZ-sj}nFY7@e8gMY7bNW!f2iMdVYZ8d+Tyh&XyG-|D5o-yxC&IhP^nFy%0Y!H# z#=X;#9LDT$_vmiv>wkv8N>2p)w%1Nv$CBJHn}yEOIgkoG>XE~hi4Yz;%cEgBL-vYs zUe>he732BBo$9oLfKj| zxy{VA8ZyV;(aU(Q8!p^H3i?$ z6OB1+AF92m+pKTvl<&Zt;Slxl0@k}YZ=r|~bFxMmtMe%Y8jCe@yZeX2)wTF6zllz) z70G6r;A5mOwi;nWQE%gIB{A#yv;EwYeYaG^=kvjN3eCWCRS9d)@HWr8F!ZM%;hUycm3tcX0I0X8enpo zH9iDmDbI{y6yHwv5#~($nF6O{(`&-<9FDE|G9TG^`yN4VZ=OJN;&0RfKCgx{o_@WOJ{>4snn7-L=UEVxO@%kP zUtYn*ocC=VCHF~AxKd54?-m2v(|3C8ybYVh79VF-%!uc>!cma^#$=V0H*xr=Hy&%I zH5lW`Xy;HLLze&bZvO(uGVGmt)M^(woDo2Eqk7X!E}O?7A+?_AfcE&19xBsDJMNbm zf!QYGjSV$)A~>Cnk3O|^vd3rgMnHS8#rlz!D234aw;crP;kB7ytTO>Sj=;X7cLkZP zuKy-ka25mzKLkSREU#jcI~_4*#;77#Lzoi_p@Dr9XO5l>9-~IpdgfN<88>#1p2aJB z<}NTpkVMF5`QAnX_3>LWQG2F2BrZI+Y~h-=V-&h~?9Cr~^kj#uI@D;Gvfl<$@0_RqOR@%^C&lxe1uI6u_%4%9A!N_ zKsw{IQb116R&t%uD?Ed1KPtl|hCM9kKs z5%bMsNWgAuMB{1+BV8;4sU?vdp_ROAe5s5#*nhzn!s&ag(smNwjp$+4fyRs6?8$*k z_cfhq*^QE|ryL|}CYo}!r{jjP*sde27%X^-3mHM3TFLVkWp&(*^FPFP`Tu&%SMtVv$1A zW@Evo!z@ZM`Sr40CN5@?Xy)E3HL$0+kw~(k^#smTtF`|mP%&}8p;Q^Syv@B!i)#_T zzxdQcTXuDi?q-@0=@Z=ulEa(bRsj$hPY3?^NP={nbQSnO30V4lcMai+=!NxgXZqA( z6fr?;+#f9x!d7#(MbR|6%)kd;NtspTwmzxjLGp_L_jE>I^!IY8(o=>3pC)@Ka(6f{TQN1|HZBS&65} zNs;V^oXRZUt`MoyYw*FG^eOIO)u62tC*e|8VH|IiBdZtInSOZwhYZ z56q`PjwgJJ=YU_bC{uIR+Z}oU(JS?RbV5S%ZtBi;9*sBuIT+tu8vHC@=Eqefd;W;y zfG^_HOKg8UP{`)3`hWEX5j1PA@NW_SI$uiv)k#8)94B!C&fpOF0HE;Zd$8HrNc4KkuX5|#7kcV4wlZ4M-KypS@V zQi>cs?SRzYkt&no-=C|!97EzD6b@ZZ@#>C+9Fn~Q-1+@M;^M*$wvU6x4>8|3#tqA9 zs5zHZH-!W!jdj#S?f_Krugg}#9NG6eR4X0i)O}#9ZqW-5t2&al%Khr%6gAlz8eMoK z_&45yGW#vK=-SOfGZ^$x1>}bm<8R!Ean`FIu}Sj%X&a9;87P)ClciFuV@r`ESl1-DMmnK9e3?Pw-(QfeMH zvq$Ax-np<_UY$|F(>Dg8n>I;U1vq^Hn{)D2c8|vD^L1eq`{g(Lk#&Z)`3SNxt?Luw z`o}HNqo-}$a+icvzQ(J}EK+~%E#Ma!hEGLz)0LP+W;X#7g@mnL>rsj>LL%be66Yip zj=VMBuPY*@Tx4ufv##d&fxgsC@L4{LWZmxMEO$<~1W#sx-M02u%;Hw&BR$Qu$>CVj zJd^$_UlVdTlKw=whpQ~ku7(3P`u(8r^K)sf1SSk`&ON+G$^5SJwC9*Njed-z>@DZXU~^Smt~kO zkXQ zKnYKt9j31up?!oG$%vScmAAafwrgdawN}hho@IYloRl^-hDcX70JOGdiuy%2?n~M- zn~K769h#OGV%p|Q)7DWR5*WKNcom7KwePpBn1&KfkfHUW?AYd12|5`Yt-^WNGque| z@ieQUStkI&ZjdsX-`yHP8J-!BNwkcGp*TyeNO&mP!N8OdBMOsMXRt$IR4+60k;+rG zGxkW2O>!`07$5WXmF_yJl?bhct0eJI=Rns#B)L&-aimD==V8?LWRx5 zdqd<7Jynr%gXx*GRYE85vtg){`OKOA`^uw)^Ic}lm$$ZOtB)1zG8rtRofaO~aIQX^ z>88u)D}>6)K3|LEr?w2Z5L7n>FsMi9)D3VQWJf zaiela0id8ECD*IJ@}jqo7j9iSX7M0Khk7zktNDfJrq?GZ%?DFu0vfA|qY4U*qZ@R8 zu6D}gcHJO0%36!H&*pl?-jMz-$@wEm8qR*7-4YVz*XviQQCoyZn{~Fi)Y=Zo73Y_1 z07*Q*;vWb9A_xC~jp$VvmC%!tc)uXeq&#zwz%RJ3j(@Z+n2VuQw>h!M`Be2EM#9d476v*q_yFTewWV7A{!xA&!Ol@^(Co3SMy7yhiF zIs@aggEX$IfttmvZsaa;X>g40=ue)xD(mk>+h3*W72B0u^oZKk#PjnW@(^G30ikRk z*q@y>-Qfpmo-zvGkCNUErHwe6Ps_;;&_#BPUwK3#+n@A?2$;6 zhsylQ!HyIBj!5b{sQ{I_L&IWJ^J?5)^BEp^;4D+OIueztC%Zr5=F`!KF4^j)dVU_j!hq_4?P(mg!}9r4C|hnBkfM%m zPPG3on$vI#l1s0(sRnMok*e>}u|H8vR^zN6&W&nUUz!?LjvaB&w6MCMQEXi7QQChz znAkPW_s$TL(YPNp-Jzd9d^iO~>qMzqpjDI10?|bjeP}Q-U(6qx5mfl3PO}tpBuhos zV?}Js>O@Ex!!t0`Do=T2U;A+IZ<~e}J&Y~Ye))f>8W%Inb)3vtS zMC#~sE}Bn})|+Iw1)c`Jc(nY2tY%XXgf5^juqR-6^ ztyHFMVjG8!2Uxe@oNB+`bwk+>5mu6E=jhco8C9b67t|FqoBG}c%{y^sFdJ(fXFn%1 zue-RC?g{k_B(+EO?N*yG7NG$VFtPG4<5Qyv;tuQ;D|$jDeV)bPmz%a4l{{u_%uN;n7R zW2&Q1*b4-*KAf=3_JZ_!b$8+3hqB!leAS%K;_uyXD#tM^`cbpq6L#R98E=h~w0xq- zJl(*h90$nIyyFXhHV?E63_sf>P->uY9MJSeT*T|N5VlILi}iWgbp&k9gCAX1pzZ2Z z^4&F9>=6U}+N|#rVFqMDA|k+y^6dCfNx2TB^!ZC+DHEsmw{~ZUffaa$iTp{z)7Z9i z86u*Qd&rCXiV7YGW;{7*oQSEuJi|@R{3Gt>^R0aUFXB7I1Da-mcXo< zs?@!c?WXJQQGn8c<#exSc9TzF=ij*8R88{r-nH3UOm-sT^>HFhKjP*q0)`g}H>}n} zgP{r~SQ6>5x@*cvlr1eg+T+0YN%arSYLkIbh|>KzN%w1EgF*PM))QN0Wz@dU30G|; zs~$l=>&@P4eDr=5(E{vaX_=7O;b@8GSS4@3a68O2Z-@)yI>OM z0$K9fpY?uY_TbR6jM_l&kcZ|?T^+Y`MdGye?$>bK=FPSn^*oXgpoEUh?3e9UkzCdOlU_=jr3^ z=BYmAj~X<6b)BodYj71-*4iCYE>Xo}F_ocxbjC2CK5Hx7xJnj|!4rt&?@C(_S1ZjVj?sbyk;m z8qq}n`xxNDl^*pYrtgx6Y*bb;4TB1cBYLJ$K<=>DWrZTg7X++OiK~GY3{x5;1KwpG zappyp+3Ldr@Ls=?(?}F*O5dI}9ZiC9Im|qf*vK8n(e`>{r5k3`bUJ!L1vb=x5}IXF zY8M1hRD1=L6y3HfI_jx+-bT&@p&L&-Z2Nu6cvWk)a~^|QHTtv@e%@v)y?bJ0MTT=? zWThfJs&{*B#*tfhs5iw?hrPysFF-`Ef9WANU>?lp6W0W@&YTqwet=)w=h*Vs0&`CT ze(7Y<(IKgo3?<_u(L!#U#3Z0p#0KQom-}l(7B))YeMJ}bEUG|nsogT;tcB14vLFyc zh7!*sEB$etMkKcmV;DhvlGMM)8;DcUA-uX9R@_6L30Pe;!}(BifllknyGt3flEj+$ zG6=!g*g}}8kvMPIEP~Xp=BI(yO9SFPFmrMuE80LyrC$d;O#hPDAinpUin>AjIy@ApbjBRbdbM$1BL=DNfZT7T@>zc%C4FPVkkt z7^^7Kt43KKmJkWt&28vh4nuOL$Wzf6j(01Nv6}-KQg!9Cj>Y)mxmwGs156+(`Yb$B zfbv1HC>@I$hi1@co-@D*$ZJxUVP{a;i90KKn}>HPH;44qD>wyS6=bSk=)>_{BHGPb zg2aGtqQb@{hhbZAD}fg05(pir*9&8{({0y2O|8tvh=}UnkE0J5aIn>uj>Wr5$@M_W zX3l)M-I0+&;cflk>{@C`W@7l;QiI>a>n$ zlNuz6-rqp0!0Yn#&(^YhbvZI3ta~7sNj$)~UP(&Ea|Sh>>e8u!LGuFMVjW7cKTAa4pdgLYXff zJVND^UcA{&`pjA_V6<4$Q%{#~-n0!S>JP;nlQL}@{ogr^i=9C&m# zDZ;L1(PM+Ze9&k-n&^l!{gAkQsutBcf_yO?yR)@@Yy@Cy?Jnb5o}J+nDvj4#^CA0{h&0TOX|+HaJ;NPgZ?lW4-JI@ z;b={7d69Bh?=O+&p=SW0W=G?#v^4@?tbS0#RK`AmgUC2tE-kAZdBv&Y=|h?g_-A#4 zzRU?Ogj`HcEA~uhwCDn=K0Yhn?rw~WZeT4SWX6O40CsSaN!rdr+_B!>=6&^;A5fOi zE33?n!jjghH!^W)(Lx zj07?~ONxC+T`#y;743+gbB)jW7<{)?2E`Dn^ZkyGWwtk3a7-u7-@AIOO1`=Vy(3R< zEzodfko7IZjS{S0C6}Oa`u^7#4AaQJG0C$qXYL{sKYUEaj6{*%7kxWtz4gY8REwN}xK*Sq*K#Z2_wg2eN& z7%g$93ens-ERt5$vvU#uG(Q(aSMXby)vw3X*?p(bz-V@oyz=#(WBq0fodeICXqz6G z?4@_!z?;|8J$n*tlp*EV$W|YoR(o($eM{P4>Q~mgRe`8YH3EuHWlkgJNhicgDXRpH zxvuY{m`Dobc|n3Vs*qUAJd0+%-nm$5JH%JDhKsIWj1Bd5M{OSRyJ2NDS-YQW zs=uv_$W-+5!WYYv-j-afXG#pP}yI8 zKY_^39XzGocPFg8D6wCBc2>6@AH8iwW#TrT64X7iWH&_VB1}~3mEr@N8!FO%X1l}5 z!Hvhf+$5jtam~9ol!K7PXJSUexNlL4T#;5GCUVX*Kmvof>5oz;PR`II6;|0xjZ&PuVe&}d9KMmWjO zlWlCzujg0g*^?r#dqd-zl{H^P*IrC)_@pw=zq3Q_c)J)W6pC{*+|05iyYg?6FVo9R zl8@98DhZhmZ$gl+PluYiaF_fGa`zWY4Uzi1-{q@6a4ZNby9Ckw?K>sR3UXvj)8DR}Gq086*hF4eISMa_Iv4%zT*31@8Cs zRZSyRFwu;?+*XnM!xoFM^@V|i)sL?YiPYQ2(^D&q%b&xVp2ZVca6>JyA`&5 zB1KhvvWtV)9X@jl5TH!+PUd#NB_P?`UQ_dz8O?-8IZM{EpJe**uGp~!5rPL*E~Nq%nOwR7HH>Zbr9jrCGA@sbBL zY< zcNpvk%+2sBOadbUjmbSmmpvuL$HwhAC55ur4JVR@31CV&gXvD~tlL+d+!T>M0y$|8 zGCb|@f<8QqQc1~42qIc#T0OXEN$hYIEYQJIBS0biC^z~mylu**DjaipDGE+`OfaC? zRx(aux!KdGp+NfO$@6FVISSVGFpFN~eDipV54BTl7~=3xGYybf_JcyN>J?qc>^2Ms zW%&9F{g&|I1M32B{KhDQ#5OPiC3s#qugQ_$f*m^~vA3^EglDBJ^TD|G9)EH7c~JYq z)#=0yy$8Jcj@ZK{y6D?%cdWhcrLP!7o{x>5sg_Gr{tME0Av~F>RS>m9-KzW8HT7aX zgP9Y@wW#Py#MaxAPqThA-w~EM0PS?DirKGf6%lFkNyL0IDbrV$wP@sVKp(?-4RiVI^6QYY%pz8sei{y z7xdvDmmPke1{7gO%Qz?J!yJsfRAojO@Kd#sQh%hlFaM81jQ={WrA(#mLEk=^eFIm-k?~j_TkdPk` zfZP5VS_|P6Q)M#$qRW;T%BZXQU-t284T%I7N9Ia#Nf+N_?OT4IAQdHL^4S6?|2AG3 z2vgyGPUd1Bck$TNWgugZboYS(Ak9Rsw%61vInd&AC(TSy#Jy>jJXZk;ApS9w_!|k| zh5`jpx_6H*Bp6f5v!^5srrrZ(w7*oXW9PR4GUjV-Gp&D=l7%|tfN$9@<(q}s=6n{G zJDYJ+eAh=a;Iy+xIp!@$%r0tqic)zp|6{rt+YjV~`?Ph3*1UcjQmGfKxufgz^Q}BB z5g_CsMiRW;tWyPr!8`K$eY(jXQ#Bht=YjjmW=Ej(rBL|fQYWpO$8t1XbzKHK-&Qil zV}9*CH;#SKsHq(H^#b3}ocAT8vtJi8RScA}+>qMZT|VF|MR9Bj4-!ruTyp!0v2q~jOLlTtH+WhBL%fvI2XWtxt|``KDhW2sKb5wR<~T~*X;x& zVR%7-c6qKav_X8TilQ&y<-Xy z>ahncgjb%OGMJu8+u><5OpN0MgNnZLm>62#LG7;4ueY<}kc(jz=E-3tR<}}xKZr}h zmhE-g%NH6ek_dqw2|*!y0WjX3*sNIVj#{%W2;%)wM-hqwRQIur`tx|S-(3^&TBPiqbnM}^s62JvHElu-vr z6mwe-cbEfiBt(%%NAjmIl|#UQ-%7&(;Ii_kwiNe6o+~xm` z?t;HOqtJg-|HA*{zi|9Qs!S+)b%&+&PyhJmExo~7?0<<0CglI;TLD4u{|z}ji`RDU zTItjL$udW4Wg-l&8cO`hQpQ+tx+j$PZ9!!WR@TS^Cy23I1Z$}l;>TAKp9jFFmCXuO zJ1se)efzF(+#g8@36+>!R1$r}7X?n9^UMKaVTts`NC%lFc*(hoE$~f~fUWlJ!Cw>N zzy&9Zd||-8#eO+MKK?%s`7f7AXFUi*RJ!_f!?HIGng`GXJr6ba09=hJuo-<9zYUSs zA_yG-S{lQ^MJK+BJx~!dT5386A z#cz^c|-(5&<4@9Al*oMvEV zGWKnRgPG8?k1~Px^IgJqgXq)W?iiYrYClUxI5*b_@2gZxlfDwItoxXWF3d zDf1J?mYFYu_^G@gVJl;&N#ofZm#M6M_E})YJeIc~r6l^PCxeN#qhaY26;_<%OA6LY zdYy`FzCPjHr`Che?lpdf>Dv9t_HgjZZBshF#cs0Nf<6uBb2aIxSr!=?4e$eR3;0W1 z5`L+FqB(FnAB>=cHxYbdSXh}AwSv5EQPXZFF{blVhPfHjc&Mwf=QtF_;3R!YeWc|% zw^gU@sqQg=%!WSGd=l!Yv>!tsGzOy3!SL4UD6g>=IU#AAx=-;}*7%li4sje-y!MPa z9S!m|KTU0haIR5YNu5OSXwzcge2lQr#Vs6uyy~lv3q5QBfsgQ>1n|US!E512&N*Kj z#kWO>N|1Xd;PY7`B-b`-Q{^yUty_WhzWf_d zdC&&q-rmSE!|+h=n*6ij;;&?D!%NjJaj*@oE|n+0DBVt0hN?kMI9AOGn4ADL3gbD3 zI8*OAYsMMJb^sN&>3hCIWZe7e+|vnZ6TeQy(9i;anD=FoqwcqVKIi@tXb2f>;kR|G zoK8d)0$LM;jUl?Yxh{TN)_y^pE3|IV{pC&#q^7#01)%9D1Ib4nU#nn8i_-YBJHeAJ z?E(T-H|~J{@ZMJFYCtql_ibaMX)w2hg$v`ZcgI?OIY}^UMX6d}&xl!W@8Jhech95E z)*ffH102T*xvTf+G@#gc&lh(sP1!x2alfTZLs=~kim@QQ0j<9Rxhhdp!g9NWDE*ptk`W$Gwq ztov6*<56gEl(}+54b7W&I6xkt_*v8@a^V)47h4})a!YnI4w3l!CSA#Ric*DKb^uWG z8l`I`2^+a#me=qeU>3Pby|ti9#H`8@xorI6j^fqP!qBt$u7nia7AaA8dA+ZUDsyj^ zF{f=l{+OsTWw0x8L(bz%J#59Pt1l$p>{+Gkc1+m(A-raUUrdXw3i#ZL`eR|8ebSxI zJvQ2(dZ>lJ2vwN40yRBj>~`>@3zD6qTk;dzx&>2+88q^;r~$+N&Ek7>bxFyk z_(!L<_D4P6vj6MS>FeDs_5zOOyFASLY+skwq+GCO^MDDEpRPyC`W>+D(Kb4Ri*kAO zJLA)b>#At<*!rr6r1|<)_z{neyV+(9Z;Tr@Px=M-@6&2bKeduLJIFaHBvg2|XT!iE zKY_P~Zp`>bt@^rq-J9Ax&TWf$@=f-hQ67|kFfr&WS@$*pMfu({b8f(L%AWMA7}h{& zP1MWML56^?2)|SHY!lb>mk8nrqxQ8_+~Yb3w~wqbfW$IP zP|1dRYiRcN3kyBf!C$jD%z-~<0OAGUu;RrS0_S!?uJ;)>Q7HcGw+P{}7qqA5#zqOn z>c$$&pCpv4_Y@PirNy;ZD|H!-K+U=4;vtpl7F0Qfjs!dV~J`-2;kh73Lsf zmy_0?N?h9r6o&8bFw*}(AS?tWUhM0F(ATLW)Q%Qm0W}bF`CjfXunhT$Evb8f7>5z9 za5s0ll>z2w6VjVQN6nnAr620=%w)!8DNW%>X4MLC7A{B9Oc_1wqjd4jMsP<{S%CCuW!Nl$Z$D{bfH{H=_?EPuS-=aW8OhfwG z2O*)$N#8vMBi+L|hzk;q>v)?M^=Mq_v82fJQvoB}qImutI%tuN3~CZXzl5%L2vEvx z54J1&`1V6wXyn)d!z_%0|VFmp_7ny(tuVol`Oy^~C&O!O=a z+X0BzDqK_}-4CQVSI8}zFryr@Gjaexs0YlqbNl_GIHe%SLR~WBi=F3Jx9Y)y-r*)j zb8GX-U-)bU1Hx!;>Y{uq@a&Oo4y&?MoX*<|Yu6fzb-j+~Y!Lc*>zj~C6HLjBB*H^K z6z6afEbh7qTGw?xEQ`Wz1GwoFU#F&8no{mzz$JUGoh`>dmV${1Sv3Bs#)@$tszFXv zLf8W1O7u&4@zJL*wmX`f9^XbaWRDfcjVpEmtpGg2;KBjw`(J1UL;iQ(x{m}X9Zz&< zOO^IqUaSyXF`|5Ux^FU{@btFGk>_Dk0LKU&(&v^&^upk9sNv>1%E&2&{AgmBhQWofy<~q|1-NQYEOU)#lHG7 zNou}au!WL>ElMEs?kq~65%ZfaufA*TUe$(Qx?&2lw(*9l8}EET_7xS`i&m26essyZ z*B=66K$p^*m9iI~Bw?$6uM_z+(tp2VL&jphjQ>Coh|xU}<$qpn>X_;&;bRjbV4B^w zboXJ>tvM8Ph;wUDd?=bRhKg&(swTAz87U97=w-B!JG9)``bBmdgzC3ZkCx@&w9cY=BEjpNWcvT8YXoc2$kIXL(ow@HIlV)7o_8oN)lvE8_2grerjrT zq}9F9q`@g_alA))Li{b8CG*UDf2F5=yVBu!8i&?}HXNn5f>>2Ub>qVOO0o#y|0oH=E4FcA6w0vZp~nf8;SfB6j5sFKDx@ zb<fGJ`+!(iCFp=W~Gz}zL+6Ft?xoqF24g72_h5iV0|Ofqq+~S0}(bR zCo-+tM}&T3>z&?Wu(96(Wi2c#Xf3HhkmkEygjJef`Wljxq3%-I_RF!GUnxfQYXFFf zGWy)te|qh`_^a&#pX0^JTMSDPQ3NPP1Lt3K)cFpnPXmtp!|DTApkYJDf(GftsL1F8 z-0t&g*KvQvA~?y>GkRz1S>^hT{D))dk%oIE=D?y#KG#2N;&)Rh1vWl~ZzV%ga^3QB zwk~=1R|nt8HSF~|5^NK3^Ek*_%(Su}pJcD%(W>j&;L&)6^(+UyHqlQK{+PJk;WOCa zCyf*HLBK^gex5D}0g#E1*0eI_*5?P0A8p87yq2^Q4#e{AvUl`%mtD+CJ@UbjqIh!& zZUh&KKPrd?JNOwx%)v=iPCxSUIJdpk8jZqq+((%00^7|BsD^7e8I{8Jmi~9UVZCm;va)& z9y1Q}vyoit{lz*ps#hl6ix#54L^v|j>KNtM8y2>Xi z!Nzo%7cg^_`M4Gl7H!&k2vUCbs9Niu{5ITAn%V9dmLNPCavhuL3~nPMd*nS!}}GNk@>&d9`j}4tmXsNMKOzTAmp4 zQq|0!GrAD#Pvu z;LMce+nR{4un_nXwbij7)~%na^KZbC5!55^^G0&#lo^9(zd%=Re^h&689wyqoG(Pn zs&^>;?+(l$R5+cp)aWMK1-?yPKj@=ubmWh90hYivv$XJ0_YdplgucAAbxH;C#i9aJ z$@6Lz`{u~wty0DePvdwu!fuW8WT(3&t_O0Ams2LMd`%!0UeU6z+_6lFE7YmC!quR;2bt~MKkR5A$0GJm_k_NWc60Y#<&y8Ho z)Fug>uG)*;Ng`T$SBE4Wbv(&wGN3kA^qZWE4=!E$%S{QRY;R1QCH?SPQ8RY|fcW>+ zzYWxST;#GYy@Vg=*_r5)l9rMX_SYE8ZHG?%M>o$8OA2bNR04_#b`n=H>#EO-Ir{lE zI5zX+kQ`yJOKQS@?uip3<+{uqvsx?Biu!aaBJP=-dODgL-JY=>0KKQTS<6w6#+zAu zxpx7kW%S)Q1Hvo^Fzu4RVcOQGTQ-#0WUOtw%nxafj#^%^PQ@r5Jd*FM_@DC8YJEp6 zHr-k@RiE%EzsoE@<)F0LfY!B+r5!><6e;3>TI+_ltSC1~d(8!A! z=IhrN>p)lbfy#G&Im=m28rm$`_xJU&Hn;%$%PAZaVDTFN5D&b&G+XEe%ls&uXNowf zF7hATJ+wI0K5{Dm&?}|b&(A`Dj-ofNLRO-k(C>GEiU2!i8DJTCnG*O3HN*z|Y{{99p60e$+x?ey# z@Qs5!=&5u>BDzXL3#w9eG)m`JHF}-PEoudj^BHB~oS|qMd{9n3&M?^jPzC+(o)-}E z^k4q=+ln#@MHcn2g5D9>`X?2X{5n9iekaK1F1GRkC)(!XXgbykI2hORBA{*6Jlygi zoEOXN2l0kY*WO)vqq!11(e!2-07{pE`fKdik(WU8_m>L*EFb41fIbWL{IfuFkyL#9 zmrp&NK)rp`9N-`%w_B&}*oW$?YJ4eZ28Um=0ogW0nm;O+hzPHvw-eI5Aej7i690pHY0TR2hIQ5M`qjnS;cRgON+-@LVEa(BpvJ z1>pSrlaSZxN?vXjL*sJay65@o&L#JNl9Kh?;HMT3RFg{8{v#=;>3sNEWXZvZvKOf< z`|ESPRGD~hAgWTm!U4(_Pjq$WPbOW_$;+vH#&x>uwC(*fQI!aQAF~QJ^(&~by+-hk zWR>3PHjh|^~Tig>VfgYc^htqyeS z)MU`v;*%7u|G;**E1zFTbpnmv1kgt7JgaR0ckCO`dUgrKOvjx^Pq2WO7f=BfPNvDQ zqQtV4iW$&<}OObYz@I4(4hL z&@b0?3<%XzAmRs{rVQZsbe&^AHH^Q>8T|O~dzft!zzZN6&K7UCu3JOJoLU56vE`Bk z$uIzcKUw*DTbXSp!lwTDu>vt|XoHwSvkof<-~=s61B&Dc6L&gMV_akV1xz+6`gerT zFmD(OChN4amU!IWChxOS>dwajZA{SwOmbT-mlQE_s{4+h7cGQ}uYWUo^rD=4Qypx} zrevVHLiC>`0dFetOBUc73zy=bu*sVHSyrzW?}hGlF-rbdTY_eQBq5ZzEZOZ}L!+zl z2)>SOx3&^~&DgP(3Pj+30R8G4rgE`~_eR~ORLJdt@3Soh^o7spllJT=Vm=fQG8 zt~7O1LLuwn>gjR|<8S7#BAhdJ&t(TIVHIh@wmMW1{;w3Xvvlq=6$5K=F4_DAfi%rE z@zkmVg8fq9-5)OnUNdEgH)5SAz~wnZZxN=G#Xzx!Co)>l0AFpbOy-Q~X#fc{^G@4B zP3^h~yW`rLpbvYO3^e5MCegpk!?V%9(A){muf7GW^@o48)~N+AF#PeaKGtHdh9v+d zqKxQ2$pJ3W8+hI)IF`+zEZ+7V*sMgdPSBD8&jDS{0yLv5y<=Uem`q1 zhDH(%v|c<&R$dUaF)`Qa*iOLhvG{=nxqP5$tJ(xS{>1l<;sRi{Q7M1yAB`hDJfcc9 z=x}r6QvScAJ&s*q2h_$_a7+^&oJqc5T5kK7q1;|W>Nu-Kn2SzVsoH}b6Jy*3Rui&3 z)eclwfHyky0=$`ZUt~Zrm)_| zU-^%QYQrh^pk(xn(&1?p3PQ1$ete3m zAUv1?UUidg106x{j&R&;>i%luv}UR9L7;1ndG68q8w!BSHU9imm>b*3e7kv5GSD_V z9z4}{5aJ8_`v{q#jtZZ)%1%b>Me7#d|5tkcEn4}zL6>o>(qs&#BU}#YlYq+b(x5p% zmOs@vXw)A(J;7`yUXoLnf6!>ji1R$79S*#ys6nBFV|hGp0`HR*WtKeed=HmKU4Z`6=Bv>mls#iqL5TI`;{P zbvdWV>7f6_vS*KwjF(n2a* zsceO$QuY?1>`RhmiY#TzZj70!Bt=L>b|FMa_H`;r)=~ClO!j>y%NUHAdG62DIp1`g zbH3;K<9R*5*YEq!c{$GUna|w!wO{Y+eewR4{(Ub){Kp6u&`VrLm!&y20|ped{>0^X z#;o1^07R!4br~iX$O$^|uVIx)GOk1O>EXRm-8+9UKexXaNk=RRR__{=b!Gc_s^ohn z$cVnig#MF6=VIBn#l!fS~Kg4&!E>$hpYsSKJ z{*x-DnI4mC`zfi85(^xmW}M32AJ6w~X;C3Xuv*Ey*R=o0j=$0+Mx6blw%F5m4=GK( zs`4>Wnr}WsjoK!5QA9{9_LAs2wp~ZwYbGcvzzy-yqI#xa?*GYj5G^6~bRFo7_f8k# z3GrweTnXk!kedNxvG1VJj+~Qecx`;scRy@Pl6s>&!@;fn)i0nrhlCV}>-?{Utc;3{ zS15BinO1%RXekavh{Hr`bNW-2fSZM?StqFTEfmAVEkE9Z#n9aim1;AAqJfV^r>p+k zYq*$?=Hfzu4rN)6&+x%Y+$3!?#IRHb0I9vHnlX=Uj%7y)s#Me~yf}QsrimBrN(2aj zl-Fh;^U&-e$H{pLVIBEbeczSnph`fpU|YoV_rk}zZUf{4f6qcYnU6Z=2q=wh;^HZ) z2-CRE@efz{b0TBg`+(dqqY{7^$C*=s^8|raTP#`+S z0eO=@9d47IN#BA>0C{C9_uBc%RVml7sNSJVuigMov0iRo$U$3a#>GcM11!EcdF2u4u1iYAr159}M76Pbzn~<#<1< zUVIEz4Uq!|ZfB)pOq`y^xi5~5SL#SP_jg}AZ+G+W3k~aZbbhFz3Oey@$>xX?*GLKE zzDbCOA4GykKF#*Pzp`qr-OTAHwo1%!N>EZRDEsli-j&D{R!#?UqCMXx19R~&jm^54 z5EwOF(xAbNbneIj#eelGuh?IeSWj5MQi{Dkzq@w+-?b+;_T0MZJ9YCx+U2)Fd70v- z-&g-QzDjvIIF~WYVTnF*J(Hai||30YtsZI&+ zSr;cNW#9SA+(x4{x^Eh_@^KE-LMC4h*M=AW>sOK%b%P7XDeMI^Wavz_T&wyWO6&@$ zhmeijqMIqC{A=e;f7H!??Ea(|vFqFxL@KdzNjxIr>0>A|QHy7=kk}ce@nDsP{9{Gc zo@wC3LJ@X3PxM3OE7_=R9RJcQzL&D5i2?W0M0E`lo%vVtpVWT*%G)~|9KU{fYZ!^p z;{_nq-V<^)9~_8_g4OtVvf7aY{QpV-ha8yPEbVc7Vh}n);TE&#xuD%oR@%(_UG%}Mk5+1U4MieqOinEj))T9dyDGXc0aR}k81<$y z<%`bKW~sN$&(55zdfmu~3e0p}7vI*$j{zUo@U0ngMPZFLCFHNSoR<066yKF-TN9Lb z>~PYlZR`=@jqvTTdA<~wmPz!VKU57y2o+Qr&4n5upgwZ*qk96P3ozgtHlbG4{LsjD;MUyAS_QF~MyZf$=l%U*BT|3zf2Nc3z zQm@=@sQ6j{`;Vrhvs+L_9FX3KQH_JGzB_&(l52=adI5X$F81*}A%myQIU*&_)|Q(~ zhCP8xg+HiBT=j)LQq`XQQvJ2R%EUwH!0g|?;8T9r z&eK-~rg5TrH-)Th{?lq(Lqj{6^FKUhj{yx3-$oc*7J$MZy>4&6vRC*PaDYyU%f^~eH{>QylB zpD#*vE%Ee_JHstO;_W98hx{f@v2af%K+t@QB)UYB457z?SZPF^qN z=Pdmv<+$9l4!7SV(r4p_->8|Gl3smRsPhaOFc>%j%%ZOPrYnLfofsR*ned=_srB8y zfApQ#u)DfWJcr@4LmBH?sX-$?^tW)3y~dmbaC?^@1Fe}rP7#T}y8w^|wR8R`uy}*c+aixBNj_BHg>NBIfWRR zj)*U(X6f}0M_?%@EKTXbdr$t@@CTbk-f89+5IJhrB@M14Txdi6ZSV|2US`m6YQ}Ji zPDNlUtGPv)L%ijqKJ%{90Fp|;D>{I;hQ{@=RVc^e#+RAfH*&(5PQOPHQg16tD22D0ce<&dCM{}NfuLo0~&+ z%(AkP#-8p)_CBJ@H6zl&#P0@%39X_2(tWjr+-Jm?J@*%CaMCaWaJ*BVZDA!?;rq9@sBo{*vKR{6Tf8nqfL61&{&gMVA*-adQnyyegL@(b5*pKWBu9&0paxN8SYDTsW=hS`5g zih(S46^+s{nKmz`_t4~(>|b63a0%;FcJ6P+MBHYb*QwFsE4M^RAB_6EbCu*Ns01A1 z+kO-=Xs<4ybT@C`YZX>?r4Wk#w=d$5jk3<=S%?`Y)P%?rQziCAQmg+ew%c=;`L}te z;(6bI?Ey44y~LUxIge5$*}?T>G<9fi5t2;jS(Wbt*SmF4gSGQ-eGo9J{YJrE@6MKb z-KL!!X)>nS2jptTN??Di926F+ZACrZ(&Vjd^S2KJ!GS^SMoc1Gw z3tFSRFzxvz2)lT?`XN`2u>QoqGO8+_%(h?p0T(x!?sPZ4zCv_)^s)LT;AHRAz|N<( zt|}({qxS)f_2l?w1raybsq*#rZ=RW+M~XFbc6ZkPUS&gn)!{vjU z8;`NJITP}hfPU!R%PkgR-x;g%BIsWwkoHWngZciWpmM?95+S62<6j*Q@HtI8=>xlg z83(arZFu*OI<^LUV0F3zDl^g^Q{BU~`B;CPxe}_`p?g8=$gzKS2%XGsO}Q9kdx9oN z@iMNRUsViv1^77gxqpOFco>HQ9F>cTxd}mgs1cYc3svx=lPzlO;U{c1xCp0^s2Gjf zRTo=WsU`)Vy#@-9iRR94*0U^1i#99%=#n~Bz7GzUN{i^ZHD&S?f#4M(>3t~b@hT^5 z?+|)gWh2)N^`Y^#YH)bigVQ3Bn!Z zGmF{B-6w~CEUlUVyv%X?r*tC~RuvD~ZvWL`3S-1s>diilFYt&l>l_f$ct;|S8M%bASZw&unP3kIA!znY%V2L zKu?dZa^7^2GKg_onI!2yRt@xn>^acp08Yk4FWc9TJSojloWN+Tet+ZL_qy0tLd z=PfnVK2MFD4Cf4`HS`Z8t+IB4CW$)o%LCDCJ!^4MurY8Pv^Iv@V7pB=Jh zzS$)8fPTg*Hwkj$F;&*m^6eCQlO?hUBy=HVlU?Q;SgTjscz#cZ8My~zJ>kwTx^|C# znI=JvXiBm)|IJAmC+*a!w?EoJV3|jV<)?rC@CoFBvADm<^7q zO|E|T^Vio&5xM8IACpdL?Cry^>M%eu&=jr{dPiC);5K;N$CIW7&Y5K(RP(GJQXzzH zs4i>JfAjP6TE+D4!t_%mNr{+ad^0#HgrB5d>`n62wL1P)7c_}Y*$H^}Fhkp+cl{A6 z_FYnw+xPgfZW~WJ+jsPRo^{J{M%R`$=?Cc35kKdF_gOhX=60TTTKFfI^!<*(y|kA0 zGDY6D&GL1k#VN&D47@9-4PX66D}sItVUp{nA*Q`!wzTZQrQOXwFQE6oR z*`4pdSVw2ME)A)GDE&k6gr(*xUr(~pOFZ#YK+IqnV3MC;*L%pXPzRZ&)jV7G~ zf%-?uc#YKgwd+3LOX?|VjK6=sj)Pe=U^pQsc4!k9Hp^Q&>fI?g+c$(P1cqG}ROuT| zI1%_-dhJga(c`QS5rFq`i=I;;xg7EHo!z-A#?ndr7=JRV;@AB@XFA_622kzf%FoQo z0!u#zC0@MYHOhaWzJXk)?+mRt<&iaRE56#Ne}DOq`8_I&8?2VMadb5gvgzfw<6$TI zsBc3Eo|%2U6ZDZ)Ce0+}QEiwoSvKr+OYx9C<;UB-$_>UUPyKdB#Av8sn0B_W9Q^13 z{djLA(r}VH3_wP20y8n{!XfSfawuvSCnmn2m)^KaWoR0#sFymtt1LL!H@R+j>knN# z;9Ls;XHDw9Y( zK?S$sBG0LzMEPHSz-s;jsC3-nRse_C=^?Yey?3)O;KalN?$&F#QHJ+ddeswGx;O`S zyd3#g4+N>%W$;>hB9E>)wRdG@$G-l^EvkjI1qiyWkAO@uOy*UP1~0SxLb`*{%@_E) z@0uh6D$&p0K2KTjLN!9N{RS&A`s4dNgrz7u|9F>ws@Xun<5AmDNl|xZl`Qh7Q`coa zs7jzgTq8t7U5Bb<;rp`D%NrQ&81&8Vogjl!8me^fc-T9-wfcS%G^=-CU2Kc5#>9|9j2o7jkJ^PkFtDqL%2d40$Cm@c34V`V>l zm!f0)CDD)!c4MSSl2YiN6y<7dek8!(qGrFwUSnMj3=ZNuIhA1OxM|R={Ej_Y9V0;{-92ZE zg;h$u9>w#UcppCVS|!l&==D$D_!oV|3{Z9*lY}7Iuh0ZKHN%|ADj=sKOp=$V1^-eqwDsus!@xJ0b=f6q%sj|U zI3Iu8`V~J2p|(z{vW|CLSRg95yi-&%0ZjjzTN>|4ghdtM_gi6lw*x*58 ze4PCDAPWU31cZ38WBqHO(avys1A+aRK=sxBod23+-_5B|{bv2p**rDa; zDJX?n*`vzYu=5mQf&zA%xf{=u1jQnc^rNY{5R%w(U z=^TW>AJ{JDfEfCo&Ksa(*fquW%Z;`@W%G>g?%?{ z#yz%=xPE=hQU>Ti7Oy9*{{(i1VU8T`9-BKmeUw`1t$|sdurof}16aaF3<{0Avymgx z3C>sm;F@JiJORP9`i?6*2(1ikjhpKjRn*({hV9t}AZHyTO3p{=$LxdpEb}J>-qv?ycIegwA zNtrNrp5yjvsj@{k+Qi6!&bGaRf+G?@V7v7Spki|z|NNW)mT!0}0f3>{hTDK-ZTGEK zo3=r348XILPWKd1FvrY7GCPwnc(1{r^ZhC9b_Xgn#Gc9ngb?A3z_L!8%lcfuApM87 zsZ=|v5znUSYjxqGgk%9~I*jOR9l#0AIQoR{%P+8>dG={k-5gYx1y&N#(@&)=SB};m z)RSKt3L^kM3MZ?W)ah1;i5D$yo@fglF6iQPHv@ptV@&gJWci}cfXtrl3N1!pVFf`f ze^M*mEW)r`g~B_sGC8^EggdG@g=5{eS-cZ{T4^DPU{D za%)=e-QN(|x$>wLF9x{_K!Oz+g$gPZpSb=xB?ALk=B}}5wH!4IvEw%;na7ah0Sh0= zH(A=o#ZoB$9K`VwwU9$HokZ9Q!>7;Dv4xSB!Ez5tCBV61*mUXR4uRdscn}W9x_wrs zX*`5@&a|--7qT37>lgsAB-BjrHec3wyJdXAcj0NC*K-rn$VKv`aXoe22n2~ltPCqe z2A#q~(?nH1vX+9J+`%QT5tyrX;v|>ifgo{1a_-`10v%I;)P}Jt{KeVag~+NaBygP2 zt~GAH&((fk0G`wnnpsdnoO+j-L{pf2nYS{WWH7V{7SfGk+nbfub_H_s$T+$ISi1qC z{MJrrZlp0Eoi%zHUlqj($k@^;V3)?gCfD2T6nn%A!<1sM0+H&mHx_OHwoP@+?FXBK z>J_fXJRk4ZcsTWXzCe>HKnRQxAI)P|wn^crlT0Z8$Z>_HG^#XVoioJzC@2Y<$jNLp zohhZH4aILmdo)!Mj9Ms)lxLy5G>bYxGK*3v9!pk=U%JIZ{~TX$c88?HU~H09TpeSE z9&}oFMBM4?;g6Na*~B0=e)?zQ4Z+oc+-3WMPrQQ(o@aUhVmVpq!5I9C_4DFTr2GZq z6WW1vnpVq^>&8bzUV7&=BCy_bzgR%CN=I90XFl!8_XAXeA7K-RG$)ZBJYenH_5(HQmt{;0ib!zG4 zAnYsCQ~KO!NZE6qr5 zzPyFk?z;kzx<7}7D%lhw2G609O57WoiclGQeF*R|YNk#-gPl$Qw2@DePVY2mm3#{A zTPuM$VK}x-=;;C|`W({(urFt)yaB}G;WY7~Yn*c4zB^wOo}vm<3AhYSKpTfW9hq4Q zCRxHnl})b>a_)=1PQQ$8QDMT_oU<`Zkqk?z0RY1Lgk8>y{r;5cb6`hXt9N|sK|R|g zHzB{K_d22bDh_oyM4RVeQ;%zh{-Od+XaEQHZ2CF5Wbfx`mg!JW9U5+En=}OaEpq0G zygSaA{wk()T1*R}UF`}g=r@@#slwaSh~D+K#Qk3YW*o`MbY2HjN!(R_BjXM#9ul05R>{Bd!64uERC4;e*i(122t z!x5VK3pUqfy z=mmSf#xB0R)Gt}!0zjx`5HXCQ&6N}uu49Qgc0H49zs5b-!YrHz2JpUEMDTN{uqmh# zxmXVXDyXh4<25OOWhkp76|ZmOE)Rg51i*>3PNrYw!gu%K@3E^}-Btz~m3gjtx6lVd zDlg=`MN{J{ucJH^j{st>BCH~1@!e@4HA4#8%VK)>o8ETVs>0tP@h^}qm#tu;?#p_I?!*Yk2`5UsKz_!7#AL03%l1dBun7)z`*7y@&B9a$$$ zmqsoY(IwLi0)Dj#0WgLXIJL1qqR6wD_%O=Em|0vqIr8alTibe0(VwTP($!_ME2s-S z)Hlfz)Wkd0hDK(FC3_2*M{OFThXc3m-qLz2iTP08K|^mHT_(R$&Ty2o_|==QfhsLO z<{UqzQDKzz$I-YFE65Ag4?(=4lc;5yKIZmWXEF8|b<;%chxLa<{0a+Y1O+tP3$xD zdHS32z01LJQ|;HESiB+gdo?ALT;tm3f3UA}pwgT2upXvMU{sY|s)Pr9rh#R}g*JTOnb6ODQ8hqh8<1%n6fb8BR_xBd{E#SB;a)oHvyYMz#edT3X6Ha`nQx9>jsF zaYIoYMv=VLv*n>x?LOxMG5^vA6A=TS#Q-{b;sgU3PW z6UF^QDvMK(q|?#yq(@~CLRIy0THDFq*@exXbxd<&egSmkJ-+@gimVO^Ur`I2<1P1vPR z5g-={OUfpWJ}*2lE;10b+-3bcoxsn~Y{;xw@P%u`D&JCpJwold+N!@P>;>*NIQmbn zC*ZYqgxtvX(%~E@=X!kSK=4vye0^xF8`4Ym+Z~Fc~?2P1oFbMe9_W)U)2H#m#bJIZ@usM|Vela#+?0&Nk$a6vG z1)ftm4i(rlnd@ouQ-Fq@*r#A7<0T%DG<37RN3K_tqX8@kCTEqbjH?jJKsHGV(3&VQ?!G4uWS9^zs;2IY>!P&I-8cq@~q4>6!@f{l%Hsd zdO|wM=N}SJU}+9Wv1RDzN@M=qtY<4-rvJCGNI`ow(6aq05mVW7*IwAz*DX!@ma|>E}lnbq%L?_P*d$Xt84Ehzs zJT}^)s@rkKfoawyy+|I3Gal>Cl+^!{j;5i2 zR&H~hyD>MrB8_O0c0iwb2RN);*W&58IKoV1M``Hx9mPyeJ|Czv#vF$1cLBLOGADW{ za5EqaIk~54UUb&G_*#`UIwXWARq~t5T+ksnMdvmkhZlBL5Z2Fvg3&-QFzaPjBQE5T zLeIxW`J4Yc3H9I&pEQ~hz$}X@jV%V1Sqtm4Yz8kU<-)KC0LP49OxO~r34!;uNZchd*^k3!xm6LOru&fniy1^BJ^_W1JYja^`ZG)}{9rR=q5hV4 z0W|Fg>^e@Bpwy`VSh4u_gMgyEQk3lB=G@##?al7=JN3enE|4BV;bI1JJ{Nrw$XpR2 zX3`Pi;RmBLm)#|0N0`*RrXP+3u{=KlhxNoxG67&OcxR0oPM?2f=r?#{BXm z?Qn@Mfz%SQU%y`6=57khdHT>x|K9NDZZ6R?^8+M8o{ZukZK2nhK#kOLQ$pIGn!WET zncXu)G`zWj%R6Y(pE4-msVzsyw!sCpM3#?uRLW3E%29rD0d&YqhUYdy_URx>}$)9Xnqjbw)mTo(z18~8&IwhHv24a4vxtq!%XsSBY=`eAU{@AL zDqMS&OnWJh&vbQCru}4@ROj#>zx0wFJTwmYe03MA9jn4xy13 zjug1M&DpVEMj;w{dv9x+-Ba7J33sr6yOC1cZEfE{NY;HXj?&=eem6>r4BUZ zr2ywjr=fNMwWHn}!;*k*gZ(m+jCtB>m#8P>s@PBHqK!1n9U-6UET}IDjQ6SKL)A+Y z-XpBbK%`NN9JyyBmZJcH2c$z6T58PGhsJQ+@)NX{C=LT!d(fkziacq!Q1mI5vXi9` z=u()dVH)nbrPw4)wv|w(%iv9`t~p-yV}t+CDOZ4t4ob}>#2<@43MQm?_jpdZg&evx zh&sAJb_59b1@e0}4h=@VFt}{e*po_(IO_9ThW%#r?AROuzkiMbdH8tqUdl2VXFKt_ zqZeb288w&j~!!suVCjyvW5sA`nM3tkqY{!kdtyaI9&Q*VcG8z`RCC> zk;)|Aa7+P}k1W|SZ>p${aSXN^&5pmYDNdTU0UhBRz&w8YqmlyUD!Bkh^lWUU;bTuq z`g17IGbmpr<6oACFb|b=Rfp|ZUN1Eafw=qU!p@BXAPg9Db08LDcAw1teDREiad+I( zL(u7*pwc0D5P+HEz@8kRF?w{w^Xq^ydxmcG7D8N?fYv9S`uRacJ9#@XDoj4W`W3F= zy9nz6nyep3ydV?9H4aP+SVgT#uKJmRt1c<@yQG4W-3_XU7bdHFU2+iS)Iag zcR0>Q`cQf$0cjW9du6zwb{|d{Ttz0C{%r`dNk8Z5C-I2sKJ2||XIWa0O2`2)(bq{} z^ySuoT|)s$fQL@@g+qR@V%Q4j-}jO;gP)8;8bm4%bb zm1W{1hs=$;xStQ=MwK2@d;r8mP;1R*nkN>&yOu!UpX5BDAGCb;3#w%nb_$t)`nv@t0qQ@=D-dbsK=P0=l*v=dBWQ9Q}lbT zpnB;J1rTDCXTDC+6X5ORAzA#xoUSR0q(sw5M^+}0u4ZGd(vGTR$!up6d{=0Bl52Eh zf8Ig-O_shzjo~?7N{lff?s)9Xr}H_Ey(7UGgvYS-i;wxBO;VeTOm_W-U;sn5NvGc@ zp7Qk>)A+4+hbRE3Vt;$Pi%Y@WxFUO=60c?Xmq{eUpiZ2EFT0!?Uu^6h$kGR8#jZiv zO?nMS%_5dGI{MIk`;Le?>k3b$qSr1xqGkfwT9GWE>&-w+P;e*E~>*`w5l|Wvcw5l|emy&UnO^lDgLd&OL18*=2>e80%(e zRE5E{@N-3Yb)f`GjRXb4IncJ1l=`iMfTazE;aQoh!Ns`-lqgnz>@TWm$YNWdV{r+F zk$1&j=K?$)asQ#Y0{zJSA@dw@zk?Hgjx4t}HpR)|Kuxn3a1c#dH%33vnC^iHuNG$p zaYY!a19>I{-HTkT67T?3-TP&P(#c*x{qopmJx=&Lg%#gNz>_srIr}7M(F5$tqLc!5 zi>4qz?hoki#o;?{(Y}+N+~Nb^yj+kv2~mpzy(MB&nl%dptw*!X2oIexIfn*qpl}}5 z_+jW1%@UgNTJuzNZTm{qBbY^xUFxz8wHU5@LP!bIn$4*VgcfLN#vE^)?#69pD%Jc( zwq%6xnRt=rLus%pV&&p9$}x{C|3x_us^cFFS-%Cs%}KeY$2{eS*ED1ZzJaFBRpsnH zgFq3$vIo}gVmGd%|pxuP*(cba2$02hrd{gncQvFpjd zu0Jcbt`R!DT=2FKsy6Fmzhs)YJJe}=a_3D}UW9On+lP|v+y)U=_=CbFU7?by4D2~n zyZjTe=d#sELlndz4-#Hh_VZw68(_}Dfr*L@!7)yiT}tFvRhwM{caUfsowbNRo+HGZTZWV|>E{^d5y%DR-( zk#>5O22VG(fL6m8cgM4J5HZ#PeFRve*#u^z-*j<~$wJiXfWJe9r9eHdKmTU`yK6U& zaY;a2B=mBK%gmwA^kKc_IxC1>(1M@o(o3+WgxJEuzT4ozr({*(5DyuE2)i72hKrdt zecjS0^o!ZPniBtb^f7o!0R*%gM$*TDWAv;)zCYxB&F>J8KcDR|Km~b#ZN%9OMs@bQ z=TU;y05L$Ft6UkDq)_x)A4^#G0(!YxgG4!qs1{H?0g|2`XlGod!UT+Jc0t5i=gP0k z!l2>$UY_}tPAC)u*mJu8&}2t2Z)LHgej9ZUYVqsJQ0O8c75K#G)6!ap7(dsQ_2Dny zD(QN@8lZIA>{)vt^61g#68FiAo?vCev3b)giHuvdr32P6cW9-sQ;68n_B=r5>>SUKp$-7l6{+!t#tS ziiSoKPRLyU1lUvBE&fu0KS3M1ik*|?0Pui1=U6(PdhLb-fTm=j31HV3F~^_7Ef7MD zJX}x$%yZz3C`--sNnI1e7{eYz9_HrQ@621Y=I8wog@h2~rP@!4o(dQJfHj$*(e9`o ztPV(!mlr0(8{fNLxL9%qT`=0Wy#^=Mm<3Cqo8H;8EC8`BCf=u@4GvZ4t? z_m_%kyIWlE?u8HYr1Cutk%Ak(S=n=xj^VD>9Xz6XADf zl=r$h9M2i6k_(wzm5=@X$!SJhJni<~5%Q0T@uL?@F&E5^Ok|fF3{#b5%mIO`_;bVO zY>RDZ(x=f4fM+KQ0yao>qeWx^(bQ#0pfLH%JJ*4#Ob^!q|01k;Ka|S&eCl_?B2x6c zypZ)hj@3LSD|GkN_qCQI7X!AlP;X=)EGK(txmy2`(?d=U=o)b}GO(w_&gjTD1Bb*gSqW^f4FHVInkpT%Ixa z2T*ZqmGg`GF>3!Vj^eE$;%fY&KMm1r)l9BtF`Ix0|HkIvx3dkiMgiMn(RY3I>~;2c z3Jl^DbxXF#9X7$HsjWY{>IkI#0fMpkh+0GtC_Pad z18DUp;I><$(^PMOEkKbpBq-ql^%Sad@$l7mcL7gj|L#RLwrB=L#>xhm&ie za-`t$GKv#gl!?oGzHSH#6aaN9BO3ez{{H7tDaL1v9npfohgbzbi{+Bzlylu%1FM_+ z%h>VnZr|@gvxK{7;S|!s&7&FwBV(H$m7c0}4ChFM|8NOERx$n*|7uEuX+G|~ZqwfE z_7nEM`2}p-{pIDcEv#<7kQaIhxb;q_Bja7_L@O(x5RInDDeHPVM08%Bu#ah8`-ijr zo|^xK^|B8=4NZ`Wo*SxukrscM9MAPec?WcLT{QQlR1k_7v2t}ayD2nT#owa+*GB{v z;BLvf3zpYf4!;ZtL9YxFdhoPD1c-T8te)L+WDq;zXzuskLzM>L#Csp@J>%1(0MU4X zy<$Xj)RI+G;<|@F7_;x~NQO!H`&n$Lez$7M>Yv8v{`Uuvxc?WY^#7mL1=R&8s<$VY zm@7Wz%e=G`B5>arU2x^?FEUS0}ya6}{*x9gm)(Bi7=o0)4p)EW_XGV8S*a1~z1wu+{j8 zqfv<1Jt|7Jar%Tydg8TaMj-c014!EO`M{ zU+3!;)^w*D8vfiQF9prBK&MvGlwfNh)Ik!z8IBP;jejSZ-P)z@$@%HTa1e~vC4%nf zPRDh=DE#P1Sza#VVJw}1ZN?P`*CdwbLSF*(3F?g_$P$+Jst@4TCXAZv@EaNiK1HAIq}RL!kL{$B*J4Qr z4dDb?KQ^7C+HpJLgyIQTT29$(nfo7JIl>k6Oy8l!h*m0<@V(khq0>Gs+@fMM`wAnd zXxC0y*ZdQVV}s4wXJFdjVdS=P@JnPkHH^k3$u~73YBPSDufQ%_5?@^IE>WR%qm;j6 zu2kh)g}9ogB*8dS{5>j$okH(hgJ&PA!04q3Q4YSjc_nxRd{d=oVU4nY`(G^$-F9I! zPZ*9lgZ+X>+*F|l-?ux)biT3W@=YH8!2Obe3j`H5OppaaeaHk}u_vTa6;onf=Px$!+m+08LpZDb<2){5)4R zS*5Jz@#u6*i|_tum6_=IU3| z60+XboT0(D_$cTFx^mgtD|Bv|d2A7M>ptUO9(pRktp-M)Sn@XGSAgYcb7-W{CB03z z+Qwh~viFd-m;6c`QvOg#pSCae)xPoJO$o;oa;98TV4L0uG4}7ikpR2x=vbP7?UPq@ zfTdk0au-Bbzf&N-J-+VMrnu#=7v9a^^1UWUO=?1TKN4iya$k0SVrquchJUCy(LWV)sK?OU033cRKhLuvM1!|SeW z|BD;$(n1$n#v@HX^DRy7QI_`Q^bU^CgcsR85|q#Gn54ctAozK!d5qcSb;lW|l*wa? z-bH7c-uBmzgP{Xu2?fmSqj#k|`nv4Fu9pJCcf7X#;k`W2-5vNOq(v6>AXrLV@K?v) zx92WRAiFwtdfqOYKl9ke=BYg@UiA5bgqArrq5!Q)!pURlHQFXp(g_^)rz2n-i&{6VUf*HQw63iD6sxX{A{+1B+rOEyo{C%Uuk!EU{nZvo z(P!d;t6!CgY2%>IW;U#_EUgw=8SZNyV_rCq@FQ=kf4gVH7Nh?{aQ;6DVD2j@G3JCX zbLTGfOwplb(Mj@K7kbt+mWt%*GnPCrB%YpH+6oP!e=5CSfVZY=L*uZ+4_@InGo&=(_D!F?@^#H&Sw3j3w0^ zV(;Sx7z4IhftM%_3v}qwy73z+(_n!XxVWwt3`$yr&fbL;=xKLs&E9uA<xERfbzoLgpVQPq?uy4mT^E~A3 znOXecVGmP}KVd{e{ zmpSuM-?{HhFza5A2QK+xoOz_4d+Gsm+e>~kFLAK~Gdq2Q)*`_YLm02*(}dqCnofbaN|!qi zewcl?NwVBpV6VjakLa0me!bfV8h3SPZ^_}9K3%@+bbP=Zf7-BZJFL=Y=@@0>-m@xF z>bYm8O3@PCc#;{u09x(pIbeetvC%3-(&iFis6~JD;{MsnNO}Q`QXtopryY^mRIVKr zp5^{PY0+2dgeV1xCnJSr9=xo#i0S8zZ~?3~OgwWFqDkM%@QWv25ShR!|h=>7Ux z^nyf(jrujS!nW7NUb&(n3tg!h4)cTi^$0SkK^d*S4(Ck!0kE{*Ggw!5G_U>m#M3mt z74+OW7yA`Yfeu3h^R|UjYp1$eXe&C8D@L*d<>^X&f*5Gz%(I(LEe!ogd0AiBMYBYV zPv=g&I4*Z?Aq!=I83`GtEL}_JE^2R_I3b>o3*1g3!r-#mMEEC)9G4|5WwgiMEXT5i zn$&&T$+Ex+hiRY4AR+PZr=+bg899YU5An6v!3DlgwL@aN>dLxvvT;b{LZ+;k!0-#T z`ChgC=nuW=BvGaMdX)G}Np8Bg9w&>U?JQHrNz@1^5SKh5o{uk|H^A*9Tn z?N3yVF@;$!_6}y)qM6GHM=&H4jC)#>!x_Q@nKKVNy6fnr1Z;LSJi)!|xMM=Wr^}IJ z(}eulf@}d$LbVL0W*hT)Zto_&Vj?JarmXU zj^T6IXHt?im&&+X3un9)Ok~OyE>@HcO&DH$Ou+GagJ!Wb!F_PTecrOIQQXQ7qwO=WJ*TAWq0EaJ zYl9xj8P5s6jyCv+7nf+H><5QI#XO>5z32A9pu@zlj*7gC7?g;_Mfp2}iD}ZjyP=U_eV!fB9#<$eVk;#4 zx)&I6Y6qIyA2PM{xrnVG~o(t>fb+c9~h#Zs2@>Zg861)cM zChZXmIddON&tkSN%<+aOIA*4k@`wYD`(!FsY_1l>#%fWQ+6*6QsYWPvj=tcWxtgO_pwRU zQ!5>_93Y#yaN}Tw*#YkuO~NRDbNxdHZ&R6`__0wPYHn)+*%;HPouA`ZNEr!^=Xj#@ z1e52JfjHf6WIqx3aB|tDt&Jj^m+sn736JA*dYKu4pr|(}woJ*5^?3d^Jy4wNlGm+< zt%{E!`D&7UZ`BURhlnP097;2t(7?^k7h5)j@;~9>a<9-ZFki59tDchWtZxiH3F&K7 zy91A9u=UdC=NIao=nGB>8hS$&>a2U;@2uWmy*W1opOQ{??@z#O^tQ3?HK*7h+)>N9 z%az7W?4tD#N@s$AFbZAjnG4f8=|~Otn$U=nB^1M8{6&Thb@PA%LCqX&WGBM1O-quJ zJ#Jg-^9MNy4P!s2Zp)KB_&NBqc?L4>Vr2pXh6#B@cM4AD9Xly)UWPDVNJmV> zZlY^d!xCWz%-!6MJUa&`j=bp#bZb!urLR!0u&D8DUCNZ#%M7%~3W`tdT1jvVHjQx1 z{iQk4_;e4__5C6drdx8PzNoZ`T%PrG7%Z>O8JziG`A~OMcRIPwF~e?JJ~KPR@{o)p z-o}=@X1fA$5;=ScTe8faxGb$L+LQCCqN%R2RTRkMM7IouOf1nQW@;ZUJ14AF#+}k| zNYZBVgWZjGL#jK!O^}8`IVsD@ta4DUvg%jAEfyeHfeuhT;7~_nhNKOm&Z-1|)>oEf zZWry?w4FW1;zms&4p+(}LMWpJ1Zm5zvjzQPN`>QHrBnDGpa%u`O5{uO%V*Q^^qsEz zKF!$_B3s;14Mb<0J-)kd4)yQ`&&kL-T+2kX1JS*0=7VcIas&w!mse`U&^*j2&qJr@ zMhPq#ztrT}R~s201H>&k+wzI9ab)ZTv`<6OkiK{b+*qnUZ7TA0S6T#RroBCA$*jI2 z8+lepY_^9p&*Ah&5dBSUp+u*o$FzA#*Rl(L>IjhusdV&UiozA&Fbme5%m_1->KmI7 zx;T27VvUh?L}r_7BR>>46rfr>_rAZLtI0o|shKscFI5=YQrA%u;*cDRz1f3vo|rNaZ!Dr%I9AXU*;hZUORy(Gt$cYFp@wzd`Rp3ZeNA4^s-t3XS+G zG>xLVN@u#8i-vt_CY1|_=31sx>628i#%!PSo7*V-kS{CpPS`nzFf}4IE+4nl3f4V& znG%1oqrk2M1VV7(iP##C-pQxPk(Zg%r|q}LWRr*pJVndJxuHfZ#^>dN4jd*mg64pG zo9cFy#WPW5O_p?)p2RL!DXmm>m{PXw3Oui144Kz9>H2yx>5B#1zAK$)MMjM5%hZk z<31p#^h0EQAD7HKy}#Pn@?y^a)83iKHGSp%9&0eIdp z%Kf5X+56cJOSk*$UmVdNM+bmIdgX(EeHK#rf8fnHe=e_DFpI6YSI*cU%ZO931-2o4B2`|jo~iw}$|du^XQbMNI*jg}9uVFG1GS%TGxrPfI3mMO1d z>+|bLB=>A7ncRTY3?=Bx9hrB#BntO9iy?-x1H3@M$*NrJoCbi4x!6Ws+kvcS_zyr# zqn#nSgBk@=WT$|H8DFu-Sj3y1s>~O~e>wN8$AjE8z~_&|sA)8vOL$Cs#`Kd6aVu{8 zQR--0bKt`GMcs^O1^brROU>k>l=f86Ah0IJY3hE%Z)+tVp*FP_3ii|`&16-HI4p!= z;}+^MCQ`VW1mRi_%b)@5_^65^^~|o3!FP?iYg6nVyq|73tJKV=t+=$)c7B^LQfI7Q zcz|6JbX1;~SX=2HIPg{<~T$O3$;*vPon;0fcQ?b^b~M z|6@Ghq}RAAaKg-j^{$2iw+|Crcq9AlyAFqQrFXyQlDx$6h6DO_WgH`C`Y!pBJAR?c zRD&D8Rse6mbn1K}Nxu6-68v18s?%`4V4lo3d-M%3+*&oXHXk}s0l;a84{E% zgo^{x)+}CL0ej^Zs6i$msr18A)u9NJsT0h~(LuoUt_-;|D`*xX)(iu?c2T7yFY8f1 zyB&EMIpT*7X>KluBGOXaurtlXQSmXiFIH1cMeT%p>1dqNeG^xl$;XI;UiRs{kUyapoL2I=H`X^WN^cK4dFd%UEgL zvvh6kgG2;s?voBh*u{QC&BK$zQL5v$DXQawed+#>O19rdOrIB=JWZ>Ob@kwC;DVuc zNPF{YG0#)Zw*NBwcT+}5t0)QE|Bc``(h$jMiT)5W^minznlW0DONSbNz(U#TxNi&+YCFS9R2?Uu6r>XP2~x!Weq?*m%ARjT`6@1T z)VVrOq+62?k8H{urU<+PG6|LsCT3|p*3>}jt^k$)G4S&m9%+}Rt6)kRPp&5P5Brac zZsa~It$KQ&>9Ivk`0SLQ96oFfS&=qd70J+&d#WqT9nOjQ0lh<)^DG~(W>A@R7sYgq z;eh$$zq-A3z&y%15xv)okGd;0-E1C1jCd&4JIazbFg*XpOd(S5Wp* zuW$85ScCaDHsCPD*ve)vZ|yW&;Z4mclPDWTYhvTM)>MV5n-`A`0`|bmvdpC9&r`cq z$pc9$ab&!lH&~N5`}E$k#Xf)Lq?q`QilHr%Agzyrm%5w*IW&%GW`mnePZFGh&tLhV z{23^%M37N1cN4*Yl{;j3>hd%TcGm>klW-z1klCRO)3nlx#B$@qo$_a}21;lrH3WTI4jgRkU1f^1WPs2Ya8_?_amd;upQFNOGYT3TNVs47~QDVMu@2@Lz zZi6yVb%qgGd&%TsUh_;8&b*V96oGGa^wy#i!LhAdNCGv>naA>ywlYZ#yri|lAOGX0 z_U-uJ-nL4wYEB*&X0yAp8mLW}YlzA_&K$yLK&(^JP&>@AKiyQ9mom=PLrh43kzZj?pKv z)Sb3*eDRK*veEYX-!6^b0vD9|;FFna2)%Np!ZSl_Dw*Tx4oH7f30P=;e z%CWs3kJ&G-i2qwY@Ly??<#2dJATXyQ$zidTODCzkYVVsbHC0;H;EjMUGqaL$BmU2{tI_{_3 zkRRA$o8O|nY34}J0gEglz3%H@OcEiYWO!{WJp~J-<4M3GYh~MO8)G{%k|&rGptOT# z%k|N+I3AH$M1_^aRwn7Y>O_FVxizi3(xqzY;y?d%%=?UZ)5*RbLgSV1X$aH3@2et~5;3q*&<`#?$P;knca4Cx^@T4cR5Pk`MDKh>0IU@#TE? z9?c%}*TZOM92qy`q5S7ZEcM4uH6l@_S|jv>?Q z=>4Db-=#t*odS~Jl3Vr^S{x<|5yl%u7aqLq@`2V$xV|K-fk4RA|3HHJMO&g&fO_i$ zYiA`?eCccBe+y;na%b$M_wR3fgZ&&>+A^TWJg|`vNgFvGbXSXG({?IXUfLZGOA^et z?(#*7V*8|V+6z}sm0E@y80cuS_B!^DtG><@x?H@1D3wcnFuUTZL}ZY(TTxqQrcdkk zL+J|8*oZ!tj60UqKL^B*p@=K6F`OSzf}Rx*l6U<^v0C}Fx+W8JQmA*x?4UjLgoXdW zJ-W#<90e&}(bmTIm&DNoln-hv=G^Iogo^6gCetBE5*Mlb-&<6D+?Es4j94rVn+?nt?fGz00_QjKdNJ}3rfc0FQ{0vox zYVhJU$twpRQ|68${XCquYyR~b{ zyS*z2p1l6fydhR)7?~m{SLg%LO`7QJk}{+S%EFrmf7ZD`O=S|VR^=GF)qUWFXA-c5 z3~DhwfOar*g!%Xs{k)U8-Q)a~rmU}nrw=B@s9dBt^o@F$E2GlwrC6zMG9;_M_NQ-k z$GCDOW$L7ly648`_2YpzB&U%vjJ_e18;uCeZXwSebS>3}n771m?|22NzUXBX>_Rib z`!{<)&prFIps3Nnm+c9;-~13A)EE|F~Bf-)s zsfW&ilUBxNOdrva(-eS!x9>vAU7ySL2NP~w7DbB(@2iB_xJt`=pE>@j5}%sTxV_8? zmbaN8zu&>0f#8mG8t>VI(qy2c$dQA{D{1e-Ed5Qo%8^<%U5e%>8BZ`Qkl*Hg|Fk#} zSJtX6W)7YMADs6o+BLFjZ5PBXS1`d0vJ>CPiRYqTXP0*w@MqgQT-3VrM`IW zxAN&)GH4o>i`Z^%n8l9F4;yub0)uyJ9W2l%?cB>zo@=2qwXz;EhJy?@_1k{8{b0j@ zWQ<^XqG+@O+V+<20=KJ%SSCo3yUH{E3UAM9VB4SXO7xS@SuWVSo$I5uf_1y)qrrL6 zj_xpA97mJRk;>zbN(t5>DQ?W!XQ-3r z7`t)8{COq=Vu^A;LyK(z%8oRz_fTUqxIg2|!%~kxvDkT z1f~>dJ%OKjU_!5xbn$fs8vS;dGZ)i05`Pt)RYxaCOBQao2y;~TfLWF{95BA!NKksk zM_cL%D1^R7WqqVBrV}#sec{H;Lttb0H9?%XrP(eDCPX?c%gJwaE#-J}JjtCx! zqz5gNH*ac|lpN6u7&D3UmQN*6l((e2!=Pc_LIF~)GqaCk44Y=V@!-Hk%PdQ8%)B8O z*4+U_t9Gog8NvVmV(w4>7mK+fuvx8EfGw9L6f*#B2u5$7rvUlCFXrG1Px%>Rbtox* z;J}Y0%0sBWDTxp&k2-&a2e@}@%2pygLz2xC+7v>~BB4^BD7_M<_>S@A!Mw8Kl|4n1 z(JJ`W2*|BaMiF6xR#HKM>6wVq*znSaeiViGr)IeHshktEMOX8ufx` z$=HZk=>S`*y_1HmZ+bCWOj4`Rqc~WkDIw~_<^i535<3&ge=)@6sOu#`Z0#2eJu;# z%dV)e{?)my9@r9~qlyTYeOg{Bixb-KxLF~OQ-dy6PlFpZ-}tl=8?q?x6}$4hNl@eI*BbaTTQJ^uhs|5|`s4>`pN1$YYv*WmV&EhG1I5ia z;hUoX>p0PEJfw)c_#wmdh_Y!2JNLYA2sgfdxQ5^ZM>f|X&?64Q$$LKSd*i8WCa2B3 zyVbg*z3UgsjZFJ~ja8MI{YQkB*4AK9raOda8+b5BQp^(L&G`>tS^sjmhsNd zi(l)Z`IA$4J0ZrLG%t-ySg+a}QAdF=1&|eqUslqqhb_@ji0z4r-)i4)zxlz$`S2tC zQQ^n%{hVVsxCW#fO|LiJ`y}FGVUI4hGl`IxIJ2)P3{mf_`hyFZbKytPrcN$ghO;^+ zQJfmypfHq%?0{VT7l{^CNwyMy02_cBUOaDhKVjk8MutHrW+qTI0aszAxSBW`z~3Sa z;A{p^GuBY`1J8wYK3A;6uSqts_mE*B%ddS_a1eyaj-yc?P1xJS+SGON1Lr@JJaQrwx>z8$S(oqgT7>p7GaTo@~5htMY{8&Zbo z$0#Ac&HJS3S&v2*&^N+uApC*aLe1PI+O`+;6E(|OSDiO_w0rafcj&NIu9>;X-2z=b zfe8bjiEPT3LKCHNY#DoWULR3oNWY9%YN8F&vc{Fl+$ACb=u@bPz`FCJ8ij(QEfZ3t zr`WK;Jpl8@_!ptI9gc43>C?3LGLF65 ze+7ZMOPWT!pQuROzB<_n^BKf>8u9!7FM7qp8hr2Lj$_i={YKn|tyuz@h7ggqy_>dM zKG8c8a|%k#ZKdeYhh#t z0fK1+N0$0`!&Y8ev`=h!pA6Q_`??!ms3*O>b`Z|W<=hP_L$BHstZtSN$#WiLyS$J6 zA56bqJK>56^YEh#^8@(P9|-_do9}1`luvxA8CW~w#Z3g9piEZOGSYjq7uLw-5l!Ss zP!yQ37qT^Y8y#Cgx_jv^iK9!QX#G>SQM1<@%d~%W$b$9W$FFJ%JTwo-@}A)#A+BiT z(<7=8K+Rm$U8>M+(nE2#&jO7tP*AaNu;15TbE)r|5)4ls(zLQs-aVBSWT3Q+tq9Q{ zy3t#r(NG;_hjd8zg)e`sT4X6t`T;Pf=YD3-K*O!?8KbPMQf`dM z@|XzxY=s2VYLC@Ub}soD*W|V54TS#V%VgVl(!V3yKo>ucCQU5lQ~i+HdD_w3$ulTy z@5C9eYC7@EhPa{C$zF_Ax{3RsdHGqENE7$0 zO8?Zc@VWpK9B?{X$W(8GR!i=cz$ zOZNa37F4OYBW87ov_EwDNEJn z_#YkbVG?W%C@%(I|5xjCyL*^ji;#XL0E)_@7E#mrxid>c?s8WwAD~_hOEz2z5&&|5 zZMjIA?lBhG-hYun_b*w1R=`1wZAX3bYUokA(Xa+TTl(K^A^y`N1am^Z1~&cpp?BFy zvQf*=e?i;;7sPhEH&*f!nbKzw@QY;y{0AX^9!96Xmi88~B?)bmU^R1reH*gg<^|66 zDm*V0O@P!7H*gzc=&6)pN={y|I}Yd~%N!xVjOKD0jW6;k3JK@&k&#|AF)RWA-loxl zxt(6C9YG7@sEO`eq5}RCb5nSzxXFn36x$RHoFmSkPU0F>XjD!?aBbW^VrWlEMTsqeK*PUA5~`XJ?J-{m1!4v%!3ai-!x( zFXK>8qFBrv!xb;`ceTtHtrEh5lpHIva0N)@8{@Z1(bve?vbyA8te93_b?}nquKm@L z8LIZt3S=QdjcGhI0)NTca%vUIk?@mIJ&ehdt4223L!9S(wOgshyr1ORm=EGTd6K!l z5UU+dQgx{mS0{*p{*B^mfzhQ^51D&jh|*i739Hn|R}dS|yZ1N&Om6jg8!iHd+M5-_S(%625i7z~MD(YEkp7Pleb z9^CP9WyTcS!Q|6zzjCjm%`uF$|8ru3?3-4K3IQw4{+!$mpGg+X<9Wk{Jh(>Gg-M4w zpTF`&BO!AH_t#Tcq?{h2J?0pc@J(757Imf>$|8XMHwW#T|R?b4n#qB)%@ z6zJjbV21zWp)I_Q6fSBJsR3iz+}}gBcMtvosuh#r3Eh_@2RlPkzRlt{opj%7592vv8vg-_j z2~E3~K$8{ORFx+ZH)V65W{Ec%B|!TEwA;n(iG+5}De)@- z#B#Jt?(i*n`45Nvp8#Vv8v_ME((z!`?ckSMq^=$>qulCm`C~UGn4R z8>Jv9XFjR;?{ya1YQoeKUR)v2OpgMrrb!-XMViZ#x_BdR@?Rrk!pYM`YlYGCw8y!c znz+f2gAi5pPxfBf$x9R%3*=bdP?)~!7{yBb61J08Et{CEtyj@j^^TlaD_qB}ws_U= z3^u462tn)SPR|S=iAmHzQ3iP`QO>C`c`f5%de2#q{@M-mM-OXwB35k|pIjH_I?;UL z!J^Uh^nSFCg`bm1V(^nNE-RaNg)Xp5+x}#&awX|MmQ(!pd>5)4sN2v{D3=!UhIsD% zM9yQ5qC%RmeL#2;89LSm=>c*TgU?bo>XtxQ1rOd%w{?GGHLLEGj@7`eC!8M|Wsztg zd5~bH@<7;H##Q!s+eSyV3aWoZq4reE;Q5lof_+8A$8)ICz)*dwbqZGs$I~c7Vja|` zE0**WqVC^AVGTx>bOIVJQfu>fxSz9VoQ~Smz!|5yb*s<0@%?2aSq6am+s|~Rkie=|60fLpBadR&poyP z>C*MDM8^J-{xoP zMNuTeY$x2#e&co6aBf$2VMitbydJgd%ux(2!(yzfqcRkoG zI(4iAjqSrr(gO`sm2}d0&N+3Dhw5%?G&U651|+e{6cw7x+ zw9X0-@dV8|Vp`aaeDm!A5Ns)>r3`8J{lryw7dF2rsC*i|?3Yqi#s$Wk6gyQIuX8OHM^q8*!{{sbj~-n?Oh^XiHL&rM*cmlBxb&>9^aE} zu2@WXjaY7yHPl$8lsXM+&a}kw1?^Qic}2Vovq3}r%Rt^^xl$w1d;>U^V_8JQT+Uq} zA+#)gHtYnAw_G8&(M+Y_;ha3X*ud!p=5^Qc3>h#C{g}PtpRn*oAe?HvBW?7}i9MOc zM7XYG-Jhlr-=J`fqEcUElX9g~&kjXRO62CduC$&Dt+6uz#oM|zhjL+I4V_%06>lQZ z$AH)vYnIBB<+W;d^2bPXYpKxn=)ytbuFGM|atvGx39PP`*d#gEss>ErrML!H2<|Y6 zF2#2+{&4XP%^Td=l1cq6k+fj`&@}f9!q=hzQwW zhlsp3eVgX$%GuIEiw$c%r>~Py_tWx?854SedZx?R43IARv8>55b;A>u@uAjq%$WR2 zgCV#V)l1~Kl4Wj}ECSaFt8su!HvtJi2wn-NhuWnb?(;Xq`S$?fn-&Ia%>AZ!^k>2+ zD)L7$q__b~Zr}s-yhVE> z1}4T4Ihw+;JtS&uLJQImF##GoXK4)#P~jT}OEOPL0|w;bC4~y3$D$c2g@lwZOQ=!9 zBThy9VCX}d!0!-DrbbDO^^70IE;fY`8Te_izc5?6S5^NY)>VFR*pfC7peglVz{Q~F zv4ibL^$+F8cSsAtD|c0>YIye2`^kPcbNU~kB?OXBq+>TDNSnE?j!-sp9yf(?f+;^i z(bc=Z?XCIEr6qlpne1E5so;lai@&ygn}Krk!b?tvE7?g;8`|loZ+moce}gpI`>DvF$~=p#7;5H;K(*n)i+DiFXzjI*=n?JFu@ic68<`#GuG*jhp$8eCM>61m2x$rJ z>;beEVj#SQA(Jj_bkByqum5Zruj0_B{CsVOW76MFJ#T0qU8b+dyaHaK<#_xCDRGXY z&+YE$92BzVOlcvnM8%TEtgFcuuA~z$YnOl*+$j}ZiEGbKTz62<)KcX4Z_$pj1G52_ z(I+h86e~l-HelPQq}OlT=XGf(1gKBSgZ)z728tG}?*ukdF^no1(EO^2h z^GH=Z;Z7KK8qp?r!}y58HfP7^U4m3bT$MD5<@?=<#RSA;sxVAtYvM9ar3rds!&GwI z*L&(}p|zE|>3Y@mu0!q3O?U?7;m)i+$#P|d7o6)iFN__}BF)X1TmBKF+ElIp{T|>Y zE~K~GoI79ZuU|s1ZI4!_#g_nCs`C@Y;;mCGKfAPwpM%-bqD93t-nC>w*z}g|))rvP zg#nQ#_>yiA0=gWK5-;QBOO@i%fBs7&<^PlivF;GxiE4`d{VO@K@BA)?GM{CII<6+wFX(_fIs?cK^rNSQ_8#j%Sk>&5n-MV5^cR7QJ?F zXIrLX1Z6eryZ}Ze4gnaIH=XWsEYAomgSedzh$==ROYP@|n|~|}O=E5ZyXU;PFX_&8 zrVC71Mp!(6r>b(RVmVO(jW)dn`68Be%y(vBdsHwCuf-l}dQ8DNiyC8ZPK{`{$jvIA9#C5 z<|@c=$AQ{BpnvpT%q{GOD>lpNh?T?Om6bzy8iSSQ!bdfPrZ#H!Y-MGKAK-C!N)scL zY6+K4qOqPQ;S7giW^;~D>-E+-RSUC!lO(+%r^aRu?Jvu`8lOe+qBsW4Kaw*`2g%aP zR+a1Ct3TNohYsfOcpw$s!!GZ$CDX7xoA{1P<>Yf%evl7>0PBq9P6UqfFQ)1a(BXOP z8>Mi;{A4x_z30~h0zoVOCt=k$Mk_tcNsN)zVZW#8obn#{2zUs?J21CgGqJOeIzj+4 z6LMuuZei=3GuYUmJRak2i+quH8aG7*hXidQKAm3<=B-Z}DPA@Yx7<`x%d+t6xsE0- z#FLDfl?Y>2v^{(!`jydO%gqzfn#k-r3geeI zap8k|Fvn=Jn0j5iv?6BAen&x=Vl%jT%zhsg!T%L7QQN1b&-~|!!&Xbr70^QwDM=tZ zEW7QJyOYvwu_OaCpqjBD627Z5c|$^=^+ZK^7Wmlkyq+)1D~qf+YRGaQt`MIAD}Igw z18%mQvV$wN{7^&$&&>z8=OpWe;Q8y4CluzLE<==jMcdOvSM!eYNl|n7u&B9-)pzqB z*ofv&GgO25JXuas_ShC;3(mTK(K|G4sau|Q zre`gaGDjaN`0rf87YC*kHT2pYid$C1M4|iK zIc?HM>*XztTqfot_xzPfLvwz7R7bsB{gX?0RK?q9g%Uos=mTuaR@SzWI@djHI z!~htW>6NeF?X9KPiwd&4lP;9Z2;?QM*vvlne3H6!nOncx>v5EgbrJJ zpYRxN~~z512o14ZRfTI?HU#x>3K{b>cyoYEY-E zw#h~ca?t(Pe7B8;_M+C77YFJtJ$KVMha{xD2}_mk!Hfxy^x%h%b<7Z9wH3qoJ6C4c z1$2t?&yWkJb-51>Kypc71+r@Eg!@ZcD@W}Cdw;!=G6vlnu36~_bJIwGNhiR3w;M@t zy|AxIQmR}%mJ(MowDW|m;6Z*1H}xg}xqIVetK|?PkKU(qm=U}L+AY>xTf9Qa!X>xs zCCIMlydXW~?uxu%{|C77U6kpK(mg;R-QRWUgwj>sJW#P1x7U$(aO5LoEayF0gLegt z|A2iQN*)alB5c@4tdpvbB<_Knud8BI#0U-6PH%`#n zp96U}GtRnaG0dga5&?OY0qhAIfbf(KsG@D^Bioo&;eMj-l|)wq@g1FY(AP3CV|{^e zk9Wfel8R%XbM6s#E<}K4c_<7JwBX_=i26Ltd>)#wxzFq3Uqk^R{C}dgh_WTEm6Br@ z)^x02NBb~bq@f9NHJ^-x@^cYyXse<5Mh#8vr#~)`?6k+?^H_)PE}DY3o(`s}@qC|d z(9sZmtC?+lll@K8H$&3DOhTIKqRAjE=hRpOa_6T(K~9y@=%3SE>)D6&h$cOnwZ;5& z0@HU@DkfoLW)um5QoJd(p_7Z@XRX$hdNwcrjyu41E}W5l-oU`_^8ctW$iTlvh_(1ihB#it++#RcL)x}f)m^Vq%Ea56n7`MyM+{YcM0we!S&_$ zfA_<^W85eAzAx`XM#j$B=j@%e*WPojIoAnRRhIeij^y35XU{&!$x5m}d-mew*|X=8 zZ(g8&8TLqaRyItwi)y^I6jKs=_N+1zEzK(qU;XfCJ{@+}@of{BOMq(3`(fs z0><~3oCQ`VjLUkK3N^P?0x4nS^h3i~Hk(O@3MBDPcQzev4BHo-frx7>lXW%Ac*`K* z3UIV8^3m$-B1@Q&!5=lwyPOuUKi$pFND&g4f*_6EldVbU>u`@dX*uLs1jp8Hl~;`Z zB>h+clF*X!w|rx{Ox()TO$OppY6JUc*Q0yQ@uY?&bP~eYeY~@FaR-&8n1lPBKI(r~ z{}mL}`*l<4o5nWcGZQz!xXA^iGcaD2D1)>tkKWo$%caYI9dYyc(|R-hqF+I`{IH(f z|4Ir-N}7};AlQEj=~B77rNjy*Od>AC2SYL*jF3&!Th5J(RpQ~dJr5j4yM0XTemw-` zqMvBadVgMoVlBRp|A?~6ABJ+#O>@&Lv5Bqgr-`6vQBi?|i^Y_D9^0!ozDmVjvbyDT z4CmJ>yvho}ii3mw<&Ou0)5^H^QL3V8UK?yt84-*Q-Wfd9+J@xpE^eMa@*z~tGyQW% zfTuaj9x4Y=$qXnu_7jyiHT}n!D~iyQxsU8-;LfaOHVSrW;h=M(Gx|?b@>_$JyE)JG zv!Rl$8s|i-V5(xllI$q&#+W2Qkk(JklYb zhQL~0%NI-U{6gTH3NzSp8P7{MoEt|@w@-((g&B7@Us?eNm<)ZljyVgOQ(#aiOn=0L zv!kl05?HpWKf?sdZZwbPWkoEeBA{=TxF$d{q*x2pSjyctF3QVuxQRt+<1WCtSg40k z^NZ9auG-dnY4EdavD*M+agg1Ln_DS@ZwotREqPLSJ z;bSiGTs4y^cVkLSN+D3-v%u->p``s~@jy%*p5ASj)~8ropx>C~nz4QB3BU7wXB4O9 zFqz2Z#BV>sgvdL`^Ir{$4|Yc=x4@+&zuh^_k+}kuCqE5s;DcF0G}HN-nz`7tO}l+3 zPpuLk=$`fu9Y|m%W4swE`qV>xD)}#tmksw>sNkfiml?Wp*Xit==^7hkaF!f&LjtwlOT1r#V* zn#5fdZOLw%ppFY!V2T&Rn|*3d3#B_RUxD^LnRf3DJ~vYDMXXZRFaSC$gFNC22d;{0 zu<`DO7@0FK|pF+4rjrq^Y?b=C1q)8X5_ZjHkP~o5A<7q&WDdE<7?dM@MzQ@9Y^z ztS)z+%(t_0$x*`8w#c9M2VK2qYpW-PJ;uBQegDmP&+3LLT8!tMdv2waiamG5u@gRW z)3G7An!G1_ItW|N@*U4;Gv}I3tqP-|@^Ph;9_Jt>k%!xZfa+RA_1;GHMU87czYNcV z2x#m~BW3F|%y+wv5Ph|rg-5Qbb^Gw~w6Ze65-_&NJRp!frYiY`Mj=K#d$|;uH)I!W?(^in6&9k zTsEd};2}~es`lET6aY$H;2f|x84nl5OoS}$op=1Re8iHO3SMeLx|N^d;A~Pt)$M_) zNButPp4lTy^Z~K*hz8L$n?$a8Ay?M3zi@Ga3}ym8_s?gM3k;FzU#{(f`A&4arGg*_ zBMaOf<-?+0OSv)u-E8v|r^v_NirlaVYXFnl9U`(#9_SF+K=ilO*z>4sfIbFa5KHRX zE|d>Re|Klaz>sWFCr#EwcM&2ODRpoj2OwId#(1?%_=MPpFFm3K^jN!kql8WY~Fi?Jr;2=G^7oE&*{#p-zl9OrB5UJWNgd6IhJW} ze}8PF^ILYCP9I|zjC?`Oh6o8Fopm`JoPVea2L2oDYd1*7Q^%9zz|AQM`MP_wO7o}% zNj@tK-=a!an~KQsp4MhTxN8mCFWGxq({Uz6=^m>z3(HwC?8Q_g8dskKc-OB zVbt5{zWz`%P_Xq#aeW~&R4o#B7)$N`Y6Cg7GCi>H=T46EwNwR7B1tYfAtx7R|2 zq8spZ*@v(idRJ}WpXb9Pqx`f#ccWAL?&ohKr!+x=Sx03e0{I|CJO0PM3)vz02&1rD zH;=C&^jc*izO>n=i=S{HT`#+N$hYg)9758954Ij=Q=;fsVhdd!UQ;Soe2q#IA|Knx z&wMYldVB*pPvMg{IN`h5?;Mc2OBcL5zOq;Rqx__iLbrc4)tLroA(&ue0=-C<{~($u zG-o-qv~FBm>gl@MP>JE8!09~DuIOv-8WIflyHo7-PD;$jO4nlE zzqr3mf3dS?@#lJ`e`5; zG^JGA`8BHlICLjl)Fe+i9cYi$m^&5s8K%w0ehu};zPKoy^cU&McXy+hnsW0dv{u|t zuxr{GFf|hpEGOOvBI#Fc8hYc+b^5CJqb81fm3_B`TVp1)g%=}z4)5=hzb>C&DZn@wTJ#EV?tkm*~T3jC&s@*VkT` zDPKS6m&>K4(w;N&uUfFASe7J3x@oWs8`F?)8kMhC;E9(6+C8lb-B z?5FXcZ9!)`&PN}WN$4}pw*ai)JDvZcX~A< zkQ~)VqGe@#eq&9==XQiJIX!=sRE|H-ujS|6!*)mz(+yWSYSfI5Fm?FyMFg^lINqDk zGqQmGj*rpDDZgsf+~=>ctKwo)njX-Z<(t2L{c;ymOns;NlXY}Zf$ABfEgZGgA07_k zOW%^zNpLpp>mv4MKPc5#o!YGZI2xvT!1al-&@S(sDmO{!SjVJe>zpdJKekV+T({M1 z>la^~tm0zYr$<8k?r1Ow9lCf6EwsWwI3dK!{`QSXwd{I0m?ER`lM zZ10X}`I7oA9j00qE%ojG8n{azx27u+;Mf^VN+rj8#g^$!4ol*%^Ii|^N(B0T-s}&v z+pJ0?G@u#KOMlJ9kreJRz>qWFw?+|8#%+9_G?m89-Z3{czFx-eCsR*(jz96lk1>-+ zUCb5zaXv9`8#GlF1~hk$EZ4tRNuHkYSYundN~qnv80tb7laEs@l=XDnyhf-77{<9v zgcy;FY>x0L*%dXlnmu0b4@=Bz$N7!(&Gj1b%7 zgYfN6n%N4OgbbSwD+~ofT^-NZx}9pH?h^9^EcwWkuhfjXG#X5itZ9(zEFde6S{7}> zOv$?Q4n|LxV0OECRZ(pT$T&1qK1h(zOmBIp$(}n0?FzSkNq$dLyU8Gm#-eDRYGCB7 zWiZfEi!n(pZYI8OX?^;cgJc|Z||an{%(7Dx0x!2H&l*HT5{Ft@T28Z5vn z=K#F-fl7Phs6~rg*oWxOm5b5NfL-2tvvb12U71V-Gk>yfL_a=}H3_*2>~>V(wWKYP z%yYTh973OJD*cO{tp04v5rh%z_?>_;Dv1E-t||%h{`I#lnUnQ?NYI9T>C*p|b2Fm5|#qM03}jMLP4|tl#L7 zH6llP^y#=0s7~5Lv;nK z^?`#b`+6j4ESm>9*?Jwgz*=yNhGdL_q$Uyc7G0QfjhZTo*!Gr%T4o#x)=+M+C~doi0|L+5a1rrrD!0+jh?tjAS^ayN64GLFjekKoMeqZ@^z^U)3pHQ zTo6J@1>S(_jD~!U+e_RT*0a;MVwweZAH8e^sHWH;ucjPU1#+UC;P4Cx`@yE5wFkyz zAt@W}6q{w#14$L2bD_p6`Zk9Lvnc>>73&MPFj1ROfZf{7vs!VpnK+nR3&|ZH&c}`} z+lI~qiZFK5e*nG^X&=qhR>l5mScp_?ff+ModLbzDg;povyRrBPnWvl^9i`+BQ7i{; z(?XYuu}s|KeO}ijSKXh?Ud#X?Cm&~os&FaY*O72<;L|>O9a;)o+E|hAQlQ^l z2j(qUSy!YnIF^>mTY9%W}XgZ$7&N7U5~?&X?U>k0QB1 z-vt5Iyw$f1Z@crVn+0zkNf#}^{Yl5dhp~NWt*6JMGcJzYq2*hPqLiS6Et|R7frCp} zq69mwC9QW;A`tdk=_V}L^GsW=FlLIWtW^KLCspxbhg{#oY%XWznx{0xmEI!W5GOz= z%ua~2Mb50bD1!F6n3Hf59>}Vkh-+$jrY+moOF^q@&*u9$NJ99+EW09<>F$99WZ-viEQ&m2<*hq-I|r}L+O-mGqYp;i0^CdOT{m37v5V11-+CKKU>8(P3W+g2Ao~( z&BnUNJCJk*d>Ix_5z5Q)58rYK^4`yq4gENMC~q5hdDbpjLL6Tx=tFNHdF)qFHbjM!L?t$EZL;^5f=x3wo1*5`yNCeTtzYHgGOvhwgCL>4Ed=0@=az-VHj#!yM0 zlPpwovy!t?Mq(;rH&UBq>g@ChqlC+6{AtfNCgcKMVtdtr#f#YWsP3+c(DJU7c+ zH2a23l~3|KT0YA3nOzCgRYVJ!K7t#_m6jxQpOkC{{O|45%WmAv#Keh(BXxQF~VO$17*fe zELF4sn26i3hZft8!HkQ3;s0XK{v7_>eWS~sa;~yM@}icJLD$68#w2=|TD$zm|4Fj7 z{JKBDM6qpZt>6FG$+!P~+V219PW*QadjH|Rj6R}~WQ&``$D_ES?1auAe^6Z8dkQoX zDk>@t&;R&oQ143Llua#{oa9s5?nM6j@$a{|l4ovbgV%4sAN zAIS&^2yioU#nlEjX@LaF!>mU~M*v{=I0pz8o7=Fm^7kaDcLNWa_ctu8)b1eF&>*xy zpdTu=wC4Wv zC*vIAf48Tso@^+_RMToh|67jg`=letO!wZIW<8a9{GX_Rx%@N)+wiILNvQpk!U-wa z#Fw6crIw3-{k7&pHgO@1&3gJauWP$$ftbT(Gy|GNfJ$@475_UG@ve3d(?xY){X=_9 zzg8KbwWdBa1{TcDn7g>x-wg|riQD5AB2zqCY{8G~Jwm?1lXt7nNDt`R8hXf_r(vG< z)8%qhE1VP^SYykJq8^IrJ3&peB`0>67Zu$-H9i!?juNhKBL{8`x*K(R{zIv@tJ2ut zn-xMz6wn&KQN)awm&5nAVP$E?h3lRI3s-;(3*r?NS-FO8ye< zU40QU>hD*V-#Ai=E?u}1eispcW~|X0X9S~#*?h$pw6ubgC-_4rZG$kq@0s-gm8Mc) zCKm>UjKx_*v0_rT`_=I^?Pgv+fELl}X|oT+o7(d4QI?_G){jg%`952mVAOhj;dnlj z@8!agHsmYtOWX2g77e8Zoq7mY+^PP$? zQx`WLjA3ZRw2C;?aa#dWOin$iJD;pEi1gf>=6=Dkpf-MyT+ti_f0cMY*`ZtQ&a{xy zgl=_CoaBR8ei&~(3FQ|7Huz4(=0tKU%jzn|pELyI6*fo;-*~7M+ zem|-9YKYdR={xaS!*e7u5Ri}2$*qChbrlTjhXe=P2^u3QA1f^i=awun`e$sKfIvIYnHD z$$n9z-y||YU9KW}9;U1@z2v<>5u%W5yYBcDbt>Z|Fy$%uQVt)TK~8e6H{r+DNXHm# z$)gH9))a16(rRPZbUll~8pL(X>wUJ-H?hrp>v2pen9p*e6o1C4@D2&qtCZH7*-q$r zA}#uzS#fRJ6(W30G`V;3Pe!|ZIU+=*{sO?-br81JLG>}$b-F~)b#DnK6Dfiw6S<=c z+E0H_zl3xbxQAZB$U8X^QB0Ps?|q;@Xc>hhtyc{?#at>hLw^70xxzsPCrT&bf$uKU zG9B5!a9`2`m^rxU1980Ws|O|^24g1>TuWt=htohxpOe)d>ei(BnwmJSJ0*`H(HlQU zip98U4o>k&Dw?WajXKhb>*S}crniT#594!hXh3K<_*6GIqhBcDWk;F_J^9T73&RC7 z59bBS`3Ah&SX%D%B_V!D{Kzj+o&<2TPGGtt5NR6gnBo$2TvhkqJP+99@xlN_Q%T zDsuW+jQqEgZH6g4_9EVo5wt2UJ@Vk6GE8TuQ;7UB`M0V}pbWvV02QOwjLY4nSdC_0 z;RsnT$BT`vA^mcZ@kQ-m%yUGm8*&(pZhd^(WwioGtm`wHrv#?5sL}xQ+Ryv$S%4>h z7g&KU!8l0bBwg(S9TA56;Bw;%Dx9%FRMxx(6^0H^EsiZ2#9_zya?ip1YCRTI1gDr7 zpcf1=WvTo@>hm54;`6>t)|6;R=`xS2NRJEAIWo%CdQeZDQc8U~Y?ICc+G!o)ik!yq zFkY>b6KlAp2fYXd#IYa_oNqVCw@ir{PUVlm?fH=6=P|sjMEW@;`Lzm#!SF-;8yqZU z-|1SdO4Bmd2%6U@PK(V`>(D?b%l&5Ul7|%mPsEA;vbjf}^FZsnM9jv#1^^!1U+|0K z_83j+S)0;OxLTQ5MMk@xHtlx# zJsvf|=54_GUINm!%A<5V7a7%TGOsuUYaBwy@*8ccYmD>V&T`4get;iNv^EsGRrZI^ zq~XM=BHcX+Pl!j0o;(Bz!;W;j2d1Fr;ZQIXE4p9ZEMAdBr+cV2Q9HO*!;f+vHm*%t zrcfQUL~?^Y`UQGTObp&;OplstOi6s1zv1&!TsGu9>I5} zp3MpZX*MHs6jcUaEelm#N8;xMs(et0&FWGAz2sL9^q zcaxV(_8&~Dfw7>7Qtdw~qz_$w89H!W?X@*}79l6NQW zp3ak;8N3k(4`fQ!4BpJ!`oQH;a{1{Jn~K74!DTNw_Ad=cuWn^IFp9Rj$R9HFxH{rL zuc&1CG*VtRo)}= ziPL)Z_2H%MrPIRva($YYm0ha$$w(614xVWUaNnO5&7oy}qokTl1Wgx!=vG^8toIp1 z+>X?0f}?D%&M4!Lr!del!dq_5H7rI-(#_>nAW4L+$CSvfb&v>=o~$m%+=Z`xJCmPZ z7w^C04K~|upQ@{@t%}!ocd)06^5-Ms8Nz&P&1(Z6W^%Ws9FCWqXJ89A%_{Bwpg6y} zOJpIFlwWc#NW==qploD4KkGS`isU_~(_^|FWi?W;sR5ZV(5l<|q-&)!dVvPa-^NOU z5#RKm?58#RjGd*xJF=s5n$}+OX1Hk*pXSIr-GJ%DbaU)6wVDZ@>CKPp$J>Tj}&{y`dmFHx8@6Rl} zPs>XJx1Yw|pnsu)nd_*qvbQ$+Vg$Lbx=rh! ze7~CRW{AYlX}fdkpFTJ?W966LSQ$CJV}ax5D?! zeR-1(ju2ZNko5DR8I5h$jC-f4AxK*68S?k#&ll9xW@b5qSd8X$KI%_KFz+i0_J=E4 z-|h=50}@@oe0PdSCU%Sagfu}9it{O6<;Uh>A?5cF&(kQk&F2~C2aAmhna>dy7xE*F-;K1&uxtVt~QZkGOh_vdXu!Spf-l``$dgyA0NT|Tmv4$ZL zAon)GSM*>Jx?M0LkRjr3@RJ(X1=3d(Bhi|2+ZRY5b7Xew+NO@qTT)ZHV0-?TL+mGbQ1AtvT=X06 zo+WA>ZsmASN>f{y9$q49j_Z!+b40J@nwlZ2>wDF$dqCQRq*=U&vru=z8Bpw&MV0C9 zv0>i*cg&Q_lkq$ENkM&!ec@P z2Wmy;@y6{jI=S6HBKK_;G^W~@u~~)+cYpz}>u!Fq3A@|F3cc2Pfuo})(oTFlsr0qIs$Vj!V7Ap*W1za8 z8_Qp&dNVzKDyBD4W9wdEp?}GE+|T)m8)2l>41X3!!p%sHm@8aeB`3=wE`;MTapGDyZ>fwj zvWK*|@3$u9XxB1sx@GLEed$St!ujKZk&~OYj|PuTH!W8c_UN*KOWh`%|GkYA#&Z2K{d-of z)`K}fe{|m>xjmF2X>hS!8K=AutrDVE*6ZKOVcEm?pYh@T%?Dz? zA9H$5+7j?v^~u9)GjgH__Rn>RNSKb+%~y3#paNRy%ds`5jQXceWa1&{9_eu`MjHK6 zwKZc;^UT3Y0jz-u@2@LC#WpNWWg@BW33S42<=NKwT%W&uaHC-R$F{>KB1Tb&y z`di`;c<(A>Q`_3Yz!Q?7aGy6;=37d|{q@E}S~DK^>UJ^CjSYep=F^A88BMbX8C9A{ z7+#TPT+|qn<;r1c7jirm|IwRy&^5g7xy^Bx2KO-&BB6EhB?#xq-;@b#*Oxav5fjHbe~T1rzYcu8 z1Qcpm>_q3rZmk;{E*iV87OzaP-mK|MH9GA8O=T5PMCn(;0omXdu(57c`AEUTQ>w_* z{x2_)Wk0Ryha0t^5b&e>2!)|j$>t#yl5>J>xQN3;XswiYw0OFv9=x9&9-%o;*O6W!X@Cj1hzhJYl`7yIM@BlC3nu#7x_c_(puqe zN|3sWm{T-AL?O|M*+M6jS!5y&KQ!0{Vt{<;O-uzL6o!rTeh7|msm(=)p6#nAK7XRTI@%3-Og28j+C?ah8qaU1IpFg z+5`+K|6*SM@pJ77*Xof*{m#RnLYuihYhYFEa*&n1j{XyMBPbD+k zw^RShoEOsj^7EO6nRyuZ<_zH#b`s&Ajo;7RBUW%dxQwgvgHAK}fNOu*{$%}uP2A-rGaex)H!omnW%&{yXpdbq5hC8aJPa*=_6pb_^T_FPKAiqeOlc--IyD4e%;qgEa)C0b%iCrZ5Vj8waOCt z*6SmcAzPgXi{AW%(s6s3w2k_rFC5zg1VSomV^nE08$=Cxl3Phe^Im@WEP zC)(uHw)bJ7&faJn*-=HC;j?$jh=}pGv`$HTx^H~=H<#ciJKI_GJAYt81`8MO&HV;u z1mWo_{dghjp&2y(U)7oXq8?jm%5i>uX3cHSOCDO+ma9db7@x>})67lE16pXZ9;QsDlV0#Wf$QsUv0OhK zA1>5`46d%u!tXlU1BOQHypS|B5VA<=$>@Ci=^dZ?n>Q0kw=QqXw5jQ|6Pz!=^B#{*!wJc1f0dHl!GJKmYcCA2_Q)mL z*414TeDL!jrYeclY-5Dmz^q`CruAsfX6!{NjN4?&vO~sDcXlF2GRftqY2h{rRyt3m ze;2q`D9u2)f6CtNBxH*}YuZ?uKBJ;KF~|481ATITNr5>@yZgST3_3QaJ{&<)#4TiD zRNo^g#Z2bRulmK?Wn(5%qbNE$P!TJL1yp};HZy2UP@*V*sY~X$IBPm{LdC=1t+}9m zGs{gmWniEj)ffGueB}P{`Y))}l`{b$((JJv<-);K@1X}jM&W~X(njJxYMJ3L3!ZLvH0eP; z49OWHZ^Gi@81w}~*)Rb4uhE6PKay$JIa{FHJH}M!?qs?=8NCUWv_4D z>1zH%m~Dd+1qnb)nS-l_*jy`}1fcIR*9Oi$x#h=b z0a02w9@=0*u&qM1CX|XtK!AgTi;H6nQLK4N1sW@tvy>nwIN4GA-jEiPQk~=P4=W9> z6ypCZ!bmH?MK5b&XIQ7mpno{>9d>GjoT>BmS0{lSXK0IV$D$GHuWKxO#j%{NVNZPh z5}4MEZ`kZ@jY}OypzJew*r8k<;XBn>!Zq_vsKIHf$@7NFouDW;5pEet0atQcUtT6Z z+q@N2TaC+j9zH!G!wx5poCDMBdM66>$pWSjxn3+OKTF9Nn_C$^m<~@Cu6}MaF)?93 zm1D%KHE=Kmu5KKx9UVDX#4mvDK`sARQcA1T^JUxDT#nS=#vG<6d>+~kO(ST*Jzkop zKK|l^8<~}qmyZvOsPKzX&9X>qN5jxnHlY5lw3yDPiZj_AD36vv+VqzrhkvV^8d%$ z`~PvU|COr!|DWHfQU)l7%29DBs-<5*8TxH8pKs?fP%$o|q{9D+{&-4ELgk{N#xMw3iAYJc3)P|a3v~raEc$gV?uW@x zD75D9^m*=IoNfp)o(u=B>7 z{#-z*9*bU$XDx|7k?Zl&LthLX-?5^iVs1%Gj5gM*^_uy5PzD?>YNNS5oF*(R3?=qD zyuH}Z@V(R(iR&&N13kfDFejd@INZLO?d@%kqs7K_VQ`4K`(}yY6*(E1qfG*fK47B* zb&r{;sj5{It6a^A@9lYIeLW%n%{x-ZyGwh9&oLh;gk}*4L-`GHPfxzf^M60lh)F7E z@>S_ag@zN^bL3)Z)dA`A%^skx5PUV7TvrOQfjs_Xbw)jtF6&Rx8=4)-Sh`z2|pyeb$H5pMR38|oFWmWDU7k|u^i}{Ly zs}2A-&$6Y)7bL5%!5w$U|6^FWU|x*mYMZH|)txW2w9+y%PiF&cR+dn&q=v>urF5aj z@5$A_2uVm#51&cqwpC8zuvlvGLMHX!zmo3g=8dI3bW=Wf_7Bv2y*WKVRA- znz+S3oX75H;Cxfyd>075I$G_O5BABleKL7^d>|THf`PS@uja8w?~x;-rUK2*(xn~G zfTwF+d{>qEvbwr`4}SPU zHG)C*&`#2gv&{jhLV}3fUTmzgzko)bVp3`(;XCtzcoubWl~v|q*+}muh&sTVPxq^2 zU3Im7y#x8MH--SR(39EuM#ajj4%=NSa$~D?0`DY_a)rgFU_c$oiX3M`F1f4D#j_S zv0s?(>5=(z>im+6+h%ff3ZgcJQ6k|rbcTh20XE5x0hg;njGl`xpg7Ac-T3jF%#|`1 z)ZU6Dr$Sd-MXfO8{#1tjQNsEWwJed8qL1s5qFaMW{t^L-UvNxE(gd&C z{L$wLtxb^^`!k`|r@W%d{XN5R+Wodi`YeQ;4GH?fEX7-s^ zGvxj~y55`j?>nPsCrEi6WOcpX1p@nH8J7L->~o~Ub4$)E2vM7+6JXfnDz~vRiYih$ zZbD4r1b&0`z5nI!@6Rgv@E?UqF$%3PDMiB4pMigxg}l9oc_C@v zy=1APXT>9DtUkInt<`zY*3%hHqwv?iKa#BS8K!VQv8{;X$~Si{lHx9oY@FT?b{|9V zQgTVH-aS4(^2L1(BDAD&*VxEe{y31q=k!8L%6#jy9h%--PEJmSG}>l?XFHmX0xP+1 z-oC}*H#OnHVFHvS@`BQ>uzxeFP;#mACG+WzwJGR&TkiMAF*%u1p)dT&Zt%V1$<}vD z?2NwY@92<79;Yy<0a{JCUZCq|H+w>kmxQ?pO-V?@d`DKFS!fR{B(VBkAv|!nWUZ41 zr>ET??{6Rv7YSv9DO}d$CAyrG&!g@~Zf2)|X9(d*9cZdT(~vMelT)lXIsNvxp$ zu+*ty?R^`J??iUu!X{kzrOZ-IkUzrH2`g%rC)gyP9aP(6c2+5H<7s(kB9lF76Vvf%ga{%&UbL!Usl>% zSrzoQNw0lri;@ujxBGibePh2vIy*qhBXkLaG}1y+B{=8eV#b(SsQo37T-X~=aH-5t z(3ORUhi7dJz4DLGuWGB+_5e=cAfL#aq8@;P-yCQ+g$bOCX~>pO+lhI!KyLSZ z^!(|h1B=hZk)-A6@ow!a4J~ap?vzyVi{WpR!l~PQFZm`UuQHxr;*^e{;NjP=U!BkK znO;s3#AfYeJH}SdUZ1Y3@m4&Oe`cZewU$t(XMCKaBWZcH|Bc=R<_S^{$Ba%8YHSzhAub*ek?xIMd>bxyt0 zIeUD#Ls=C+3Z0-^Ih=_7MI5o#n{7gQtQ*~QksVT6mGm%msh>I6odN7*e0FFN%+k@? zWd>r7k*Co*t^YFBv(TkHf{Lh||9l|*gWKQM_B@RsS@3{cnT*VVozMj4@GX=&C`OYt zAyhZXLU%av+Z?bp(l}3%IQ3N%sl5)%ux@P24&ReYdV2b-2RlK#j)Kr&pvK#u$0w^D zV;q@^qnW$Qw74gXT=PMD71UNGlHuZ7GU#l2px;g?TqV2^8VKP0_KiB3Ikj%5iq~#- z<=`NUr$x;F_0AcJiP*uRRZa~OWAhF{!A0m@aFE*VFH_D&wGtqv)eJu<)*9V z1lk8_sZP=6btk8c$u(+raGA@Z80@GV{lxm zS@@Ael?=nK(MsbJGjoaO>6$5|T4Z^P_U;MObsl$&$0&t}J?azS5AKTYZ@X6m@s5@Ks*rVz^E zt?VzJd46_Ue3g39WRGTu_A4((uhzy&+VLne){%L1#VibkYjm!DI^Rb##lV*bI2ijA z7cc%crb=~9$2E$$j$||Lxl?Mr3g=XoP@9s1vOJr8R_Iz26>sG}O8=P;+vKIU_r`Z> zVId*xZ&t>+BAzFohB5GB{mh^#q1oRC8TEKHFHqoaCa0L=p2d#GH0W$X#*Em-qQ@RH z4=3Y4yE}#xCjIy)e(p=WN4iJ!nm_S+#UBfMB4}{1Vi0xebh^6V1)TNV6oZw&qf!b! z@=l{ny7t{L5u5?dawGDo*GUC*L|;P*O$p!Kj_JMsq>5CH`1pbBol^ghWdW1v3;PfAZ=CpN zrlzJA7Yz*RC0WQgf$zJ6-prz6b|;dR-)&=J>URRf?Kk641yl%!7XBKeloJpgIPw`YvdA&&+YNi6oPkVUH?dIH&b~52zq(EKu)*Iu#fd;$im?rUcr5 z8~D%3d}?85S8`NU{bL#%eRz5rAiqswQ|QS^Np1ELm(2}(Y{P_asYr_{b$&;R1FKbS zAu3p1Jm(J%W8TotH&$kG@EY>J7OObI~nigj{SU1 zTOw91FOp~DEglXDy6e=e1s-0V#)f4XT4Fcrr|W;MW~}aHfzr(-C8z+i3GFu$<0spE z3@Y$AAWz0%4{r(|evM0{ad9xGQSZM~;K%_iczq*t9yO9KEI~7KDDwTgKP&cTBy}hN zZ{Yhn4$wrdCWCKh78rI^kv3}_`{TUI&Yab9BXdfeJ0@P%|IJ5GV7iXH?J5nbNOd5A zEn)nXETL*PK0YNt{72|8W)Gng1eKF^n$k;i!A(iRIYVA;SINFB_!vX0v^NgLFx_O* zE?cSjctdaE#NXN0*4Ec2XK*>-usS&EZB~-K+?F*YlNjWVKXH3eipq=Do^L-#d##wt zhqF#eCeN-wxkQo{6x-_ucT~^Z$GLgq>c~|o?n+7IwU+C5vhp)OpKgg<7=z$jSj4E( z0WF@@H~-69)Nxa0IHmh=F9v;dLir{5X8Ad2kXEQvt*M%^}?+x>F5 z1EMM5n^>&fW1IV$kHn7vljnba6fljyw=ez;Z`$fuTB5j6*Pgzu7#NSGAX9r-|6uio z$6=Ar{Xi!bw1dVmx$cv_+`12;#l9fYBJ+iy(nRbj``HRqn1Hc1;gJ}CRCFN`57y_agcoE;*2$3RTSk0F1RvFTiZv)l-Wtxp5IEI;N$ z$FPP)-h1TM5G4D#f~<0LT}j9|oUW^KieXkCp4V!AajRmY`8SH|tAfJ*uV)g*ab86J z1n&_1B*yP4A08gqVc;tVYa>9XT*C7rsy)U!C69xOM2F&0=^O?bI|0<+;F7r!c-Q_q z^Ng^ZY*E-*(=J!f6aNEsdBj_Tyn_S*^;!J&R z5Igy4I#xzYPB!=cu*i?!ECJ`zG|aveS~fAhWi zLjTXfB?iQ;+R-`d;*dQup4p-y4%$5km96$K=-fC1Ii&BJa9jnt~)>P8G zz$NA)^Dcp4s-41AAN>Q#Z{+j7LEa6He`3jk9<1FFZ~l+w&N3{@XzTkD3L>2|fFc7! zBPk6MLwAP=(jX;BrxG#(nZjRWes@g?XzN zcN#Z)MR?CNNYnL*>8&V{34a&59L)0&ksynMJ&*}zqGcBpE4FTl8bY&t_8`WiUH<+3 z(KWpcBTPt8b7pcC-uCcyHM+*5X~>-D*;eI~)?Pu4W&i-ZS*Hg&!FpCaMW_Z}X6Qv_ zJDO3ewU+`1hAk}^8g!E>_hjCmL>V`DN`tRb%zKCaZO(5S2M-!TT)iN^8kadSK{I5} zaHf&5ai)YXIj17K&HSC-a7*kpPap<-P)iMwi|yF{jY@}^<8dM&M8(>cuos^SNbvA? zkrV37BUzShQRk6j#+Z~#YpH^4W+O!22`5e7!%8Ildxj{}y*vGn?VlZ6J@gS&>h9n5>L!6L0g40L4s=BjS!(P4y1*8r6=S;tCU*<;!;ZjT|9wwUyMe6Eb zyS+!F?W5m;Gdle&o|%K0b|aF4l{(<7HPBt)dQa)?drNX1zT4!@Z|rFN!cXEd*16u0 z@?x+c=$nF=D9X!A`W_(C;u4chAnI(d>CZKZcBbp_Vnh!LJq$>5gN}a~f^tmW&ugo4aR$; z<$RnX*)>A-{q|VeOAsO@fdis%!u4DtRBnU?T-?3%Q-GAYtbeigEhT)6ig6E*A>gI) z`CmBoH^QoJ_85v*&&`P`b4^XnXcKB~6TM1P(Ox2a4-aqUD>*RIpas|x1oUoSfvor0 z^PwIB)NqHxZ24VO*{PJ;UP*Z8AQlm*Kj3Wa2{V#fySJGLDuBCzIB6PAH|Fo$#p&kC za2VbC*;P%_&NRR<8(U0^Cm;MSuF!iS(0iudfl2W#LN4<6V{hY!syB80M|AQX73X@U z!^|kw?xzg`-GfPsTCrL&_MXpXUkqLT2OgaQBt-tkTb3omXupyDe2zD22nqby>Mn?d zW(qHsoS7QaTg=A&hh~h5(|}Snq_tIiMkKm;nEZHmfnob$nzkMey`R&|(|$QydqMx> zjl}dfuij&C&owOsMP<=35jfG&5@pB~2la1FRx!+De~x-Vy&vOq8z$OIT3io4hx3op z#GL`(d$O*tSg;FY;sZd`?k|7w)P8C^r!PC-ElbPUoIY__ znhaa)zN97Xb{fB|q|EnB(Y9}f#Wg^0r5c;Md4#eTd`_O5mq#H}ci$4b_Xyu?geK?F zT%$8xo{<~w&5Uowy!J&L`lTM56BQYnsaw>2w?pDD79$w;9^gyLv;5lXd#U*IR1VcU zu{2lcYurVvqoc!{B=Y>%%d2({EPa>bjUO##Qlh)bFXu_~i4jkWB}9GE+7xvXYW7!#?ZiBFKajS9E9Hg2Ud!!<2vfPnHEBr>%Ym<%Zu>ql-k+PhZ#P~A%C28l2^%IXT?Yq%VKb;mHoklZGybcX7(T0#aCBh0NKn z;pO2R^)6GvT6^8+}neuZA&^!&xOv|c$Sr2aeL50|R+e6S3cHL14FBapBNo&;3AhMUKjKS>CC!K$mGn<6rXC%das%qogI$<88Z1Ya_Tc zr5KK)<_N-UNpt|vi=)HZ+&{ThxkHlD!JO9>0Z9p}@7 z^qTb}=6ZbYisHWo`r)gCYymrBVqCMt)%ARH9GSK|UPQ+XaGzR*+V$CKl9A-7wQJSNdv>41?lA zF!6@+q|}|wuI(-X-exosDLoX^@+Qf+Nl5#^68`f1HIZD`8x>Ce!^9n>+r=#+Ia7`q zxk@ks#emnLZ_x~-v$@1MSvkbNE~t3OVyGIa?H>Fgo0H%m`Gb$*@*^l`pprJERa!F2 zc{u29kwCr#p{K+R$?H<|e@lV;JPQ{2`YtPv3E>ll1e-uPESS>gMQO^cvkh8WEZhh9 z`XeIuVc%UkkLc{Sm^7T*XwDIo8VenPHRLrA0z5?qDg|$~{qBH^bsRrOi=0qf%6zz< z2z%6lu34b|^_|6(yw8ukwjd_L>}8}q{p1yO_E5tLlkkb>K!(}1JyZ?~ylIdn`q+Ri z*e;Y{6o?VmAd$_eih_{#qC2n7H-XUUwKWyT@b_5a6z+Ey;%sFdYe9Nt`3w1qCm(w5 zTJfZnx<`so((y-WxV`P(udeg&v*g-XJ*h}Cc_X8ggl~?(di2Hd#26+Z-VYBBo`DZP zQXj!ve1md|>gJW+?JcX@1g(xzb>zCwWdEPQTg z#V!o2n-`$K&T@FvbJo}Y;nGrC&U%kP8SkR+sV_McHvccL@&6c`P>IXO%n40CgOUAO zdj$V&(AToSKkc*stJ{_*occWpKO42E6H@!(ysR9|ym%dH_|Im*eeja)XFxIDn*@aj zb0iwo^S`-q|LcsgHtb7F$%Rk7sp&Y4nXv#GVBdLmB_o$y)#}?ald4h|i9EpTG>S#X z7(O?&Z_K#ky7{8c%Ykm;bHk7H1p)d>tuU+%T6cx=j?vwJR0&vRd(UAv+R$wbjn}(cWhtyHAB7@+91K3 zkz%Qv#t;0|bFp&aDja*!AfT?KpgFZPZ>erABvabbqhWG za{1T|e)qBTIlV)&J%)BCpIUpK+^ z|1>k`z~fgGV7Q1bSZ|xJjJ8E8vE_V0cWfF5oGV=8$tdfHq4pp1&0Y~P%;Xkh8pPv5 zF8*-A_$hBv4oeU#zfo6nmRdhwl66=unf5aQd#Myv8CLq2md56=h6=Hf}98 z?k7S}q$8UYCt3F*l!epzK?FJ{#bU)EZ5so0Yig4d6XX#`eh5-43zk^r@i1O!JmP3# zpz5&(xb)(^U)-#D!e$+&xsj?WnR`tP`A`ta9FUv)?5wc7YwD_D()Rs>lia7^Z}YG} z{nu_-KG0N*argi0gY6SEo4`|0(FJ4g((CF3FwskdxxzeQhEDbrbj<%d6kxWQ9+Ie1 zD4YAsn}|Z&!q&-&o`HcUdGJiV3*lNlNnj7GL9j&HS*q*{I%xyC|Mb@VmfI${Srr5T zk)Q|J1BUv)_4l?<2ng=)?~hWFY0r-YzwSHQv2fT(L|47}4wj<}paj~v`3&J)4D-i{ z>Fmv>^Ve(KIw6>^B?h!OQ;D5psULjaR#2$&d0|?plRCM>3MHEni>^YRY&_ZI`P6gX;50KJ>8wJ8ioB1eM< z*jEoUz{g9O2ry4u`jCN~x5q>(s0_Ykh~B<^8@REcC$}RaA_A<@Z{VkRZ_ns=1e}pE zDTs)RtEzsWAmA-VHJwyj^jutA6wp}f%%E1j5uTK06_G_nNv7x{ z)OxQ<&sgJA{2v*mmc}Qsf?#S=>A({_*wF+q3lLCgEm6a8{VQh2r8IW7Z&}u2X zAg*Kw;9!vk8ZLrX{=&R z4ld}(`FX&B>K3tq&!YC5gnokG6BiGd;rc2a*lk|R;k^vrpR+!Gcvo9n`}diSwX92( zw*`Z0G?5=9&82QnVQQQ(6~x6Eb1+WY;o5gPDAaRnN?XNF^FcDxPBZZ9-?M5Kl&jqf zW6SIaFz_?gT8hg0kjn+<{K0TV-m&biQ>r55u0H;=-EA~lFp_58t}bv)#Kl{|(qcSSl@DvSb^(EDM-bHN?ws0~i`lgCKqANS zf*rZ3bBtQ;$9(zk&K)UX_g7=D-cpFoRMIWguP6ohn6?b^Hf3>mn<}4olx4p6l#wt6 zl4*kBLCu1Sva;{#o<+m69|ax{#w#A5fFel;Mj7-Zxn^m>ab5X-E~DM; z(NVSkB5TH8clMtzxikyDaEFPVkK(UXtj$Q2x{fvkZWF8?I+AO4>(bnd&Tba})1PUb zIA2I`jU2y;x3tH@D>jEpK!l&iQrtGWI=akoSw;V97kZ+UQN!tln)cY`%<#>b3WBq9 z$Q*CRoi_U~2ZTPHEeTz%X_)%WshPEq{RU~sElz#CTsbAzRNH>1ao0~LRxD{L^l9Oo zS{r>#&F+k7{0v-elF&2N?D<9WQfijV0=>x=H;zgN{v1`m?CCCRUUJkl9FU1LW% zC-?g#)|g7!s1D^M#w{J{M!2#zr9i3gm8ocGlbApYqa23Egj31<7HhhV9?&(g_lvF&24wjT|5y$!MJQHz3T&b8va2a;6)RRoDzs6N zr_Z>!Or0sF6=N^z{qM%-RzcMB9ndkR*ee? z;(q>0)b`GDSfZjF+>%@uBk2{p{>u%!&rcO0Hvk*Zy*xj5oUF{;8N&g{j|0#efdMFg zFqA4lL@Ov=q(naQ`189RE^FWL-&FOLPWw;nW>Y1%XASG70{GlkZ+xkmndQ35?#bFz zMiP<=1$J_KjcKuR2{g8&Dn}lyF%)KWRB06r^tncx4G&)Zb@5sjo2on=;)8Dw3`A$T z?HV?HqU~#JdLl5jKgqPu6F}`_py|)`==@3g!ns&RTQYs-`t6?)xdRL57ClaON7X&z z-)8QzP9RThedsn_LBro+ZeY_)Qfnm(<5;gzhhxHFy^EJokP1z!bFyG6^y%N__v0*n z=HphjFp`+SX3Z8n`LYgso6z{P-;%R2a+x(B@c;If>m~a2mlo&!>6_y1)koaydnu(X zjW}av@V}?3swyB*M@SR)0t{LBy|R+9`k|Gvl~a(XOyl;tiN+a>g-9hmHQoOF6!xbN z9Z#V%E}UKD(+V*>c$ZYemn>ZNG-$sD#wPZV-z>%5a&(}?N;~0ES5Jw1uu;&(PmCkLORkF3y$CUXi z8=CL!G*T;EIZXO=oU_u(*W@(e!u>XR{@|wr2xU=>lv=_Ab7qi%3eV_I3um}Eb46CFUYy~UEXLf zZp%C^qg<(%De@6VgNgTI&ybN&TTD#5{%fj&7stv2?;hpPm$}pg795^^bxA5R=5i|c zW``TI3kWAd9Tvm}j~|FrcOF=GS5Hi^c6vV9Pt8wAzt{M)X9TUCLMc`rzryvA6kHa;bCjF%qpF-gdBDLO{jdL!@BU+0<^e|p;=JtB0I&Ll6wmr`hV9P{>NKCs)0 z&aCBbObH!NwmS3mUw(akS28&G=L$F9m{;4c7u5Qa>ck5gS1zhUa0n>VpUAvPCn^*1v2C3=Ly-}gHhq#=?PAo z*LlmHASvT`+6Ne|I2QByJ3Q?Gc}nIqbO8|xz|!Nry?5?)wKjv)y^<8H8uhic@wKBl zU!HhBs4=J5-1}HwHH@bASsbHtZ>Ap}AxUXz3v<<)Aq)mzT0@29 zB28uU2f>0u_1)J7OG2vK*B-!nM_1by*#mXz23iW6EW%^lH97 zE3eSBa&&umvrHB*+qdlJb_^M_B(Xa8j+4as6FSZp>wz_rM!6N?Bhz>UOiX9>J6=sV ziCJ&^4*jQ>t9~cChfykcs-`G61+MUGvMwLUt5sX9Mx{x`s5u*SUG(@RX$XN~LM=xj zO`m>FF?+tz5!#WQEm{@)j+06|=*+dEK9FjGvaBI2w031b@!~7la5(2uX}DH6_-*lx zL{7H`Yr(t0|3kSgQOhdXy@pcjxI8`9qJEXuw5O?JaP_*jT!jRIA9?ztltp+a@kM5l z`=bv>6Vz-ml94>ZH6!G-VH?QPmjFM(po$~;iofxK-oJkjkVHQ~+&~3vef<4ZoBfV< z1#NsMOP;#OxAP-d-O_mK-={zZ(-H)}i~dRLyZ= zufpY|-&0)ae4ia$#;7f|SmjJHGRT2^dF$fx(hijtzAEm4*6IujBq1S5R5=C|%h(6a z-I<26fU^T8h3MyAUP(zBB>O5qBD-#jUj3AroJr#RE50GA_!xQN#?Q#Mai-3%DsYxu z;pG1Iw%MZ`U+Tn`4eOAE;Znnaa((zZlSvgQp{O8WTG3H00b34M+sNu*GH?ytR|gV5 zeUfHk%3t~%1vI-lKv!Y_W@=c4010JoO{vC1t^$w6m-ik1t`@vPm49BVKpv2Q$v}R_ zH)+5)@l`N62>hAe5eyptCrW)w`m(fO(oxyiy1D>IIe`kirCxCjE7M(VC@y;x8&P&H zW1g(yH8f4a`eY?qQ2w>@b%bNskm%6_5H%3tcWB`@;RZS|&1nfKxO$ug`C`fD zK~O5>$s+2N&M1dd{@zP0>c6qR1Yu%gnlryNK1#yOG!GBAf4e8#VCfk5zUj1PTNvjV z;4KNTX{B{&(Drttvr(8rg}UYCEgfB5)X-3QMa5?>SRg_8Z$qD$m>9AzU{KVnYd?3m`gz;&E8-U+2DD~Ap~^N&<}BF&6b$*BpCfz zoFRu9De?*MlB5EFvr)HansMgv2?$uJ$sv4Ik2L9nb%{M5h9!o1m~Dd65=Ivm_ z%$^DlM<-2*YVXuEz9R-N@~;9Ay}b$u;rF(nz!G-(+22Wa4U~J}AU%Ma$pK2VhJ#%O z;Q;Enk=Z*~s8anh-dvp$;HORGE5-t6gS)qRT5Gpd~0K4zXKz*%DmGI z2(jRtWrf3)>mvK5F~hjxQ_|8j(uKHLSbhUT2gWKTbp?QEnU8f=U*4{)Sg%O<9VwZP zV-@N&gUS!Ur*Wq3exB0OpMY~WQDx3RL(>GtyWG4(4ftYYGZYkkkg7~jAOga*fGd^j z;ltzMTsd(200*%mNQ(*rIgK&^%o|Vy)-mO#92NQd(t(Z0WJI`_*gjQo$?WK8twIbf zFhskKQ0c;8L{sp_IJ=?YBsgyxoM(kNI0gW82NbxGkrAU`i#-v*|GWgg7;omgchyeQ z{6q56p-MJCKe{0uFiX@qe_ptjB5)(@_@`(aX$m-8=y|PvgXbJ5KTrkJv$4) z(X;oOQZfOdYEIH%iL)jBU!$LajRu)ozj;6J=s7hR*=3WSiAo_q91dq^zbz>oa^vV^ zbj#`J39aBWPqq+R$e}lSV0k1(Ny)%7xA^KiQS{O~IxwQ{D1hrSrr0lZ zfK+!=o<+zNFfxa~Q9TGyKz_>=2P#GWi@3u!Y1NEN$NsC}3L#mN7 z&>=z99PZF&*yJ+DyASpxl|&9k=`O*iUjbpjNZ9T1{CRah;!~InHV%&V{oS_1ZvY8E z1Q{6_MUum-IpP8wb6s+NYH4ac)p zm%kiY`b7Sq$VyB^#h8brMjqg}BQ`KUdtfQ`Ck(EFmQG0kFQ-U{z1leh%MTc&U}@Dju^7Q1H)qD!I(J88^Gzw1q2Keg(G8FLl1VQDv#fLYpS2Ak(8hXY4 z+{6c7LYdc*W&34~&H&y8mAK>2k9@ZRkQ$_MN&@u=a&6*nnlN@iLIQWt{rPhr09?8Q z7?_2c!b05(Y&rtv)bBz+6$8Hspy)fm_IhCY@z?ftOhBPdNvA|3!K;K67Fb}F-zWtc z6*kggf`rHY%j zNzwZ8Bgi(uEVCzETWf0(lW!bgqZoyYFg>PuBuQJ|F}eOUCkwUI;le|Q0{7k2moICr zyB%~AU{>ouBA{9xIZkjjo4)0>1eh&IPw2sW@N`}PNHsewJRE>jbUYZKbvUOB0Ood;AS&<%nJ0{N=qTl&ZVTuq}y+ih3Qa9OaIYWayge<^!wC93nt zD?W-k42gckObgp*rwZeLmyr>@GXM}NF6jauAt8ejJy-|g=r+=dbmD$C7HT#coNAXR zX52Z@Ru-rEd+Vy({HInq{@E;EIwA8Sg!XKhW9w+azv1QUc@yw!oniZ;8b$!L$Lz)b zsbO-Mof1$N^qCBmEEqKE&=+_W5`PT zo0goBvJwO4R|JCRrhU|6M5yI$JlMI<&ZlD zG#X_Um2PvjC?2v~m#Z&$I5-G`7Y-Oa1Q9Jy<$(oc3A$ZC=M4({3=N$wClp}1pDs@j zas{`I>OJ^kPNBFysFy7KI*0EyTg-b{>k<`=dy;OSm}!4sB0ilr~}u76J)OGkQHj_>b&DwqE{}6CDDXGau&uQwte}2tH}U zhJ}n=bE2v9oBna;QhIqJ({kB0HCheGChIiE@&-s&f9 zwYP*S2oywM7X3jgQ_7@kQFl+z{^azuG4oex8)z4A8sjU>gJP+(g_BMtkl{8c|Ca~R zwo3naijMD;*nZMjdPFXv&8KXs`+2eik_O)jR?$T8yT6^3?vNEOg5B5e5ZE^(bg}>P zaxnYde}=)oslxwXKA86t;IgO5r8;HfX~ECf)6t!*cHzuG(nlb0uf#+}BbnQ4RW-9X z5>(i86(1x!T#czAJ6j5lSTAv}RG>y`;<>2ts}XDE>tHFytE%+jtHdiMiY5KgFhNaW zM${+s^+yh`ZDn0=0f3cTQ1v|)=F4)Gx+Unl8gKp*_E2A?%}v&7d7fQ&bYwLPQ8XVUR%-B&Y}|7^?Ip zHPplrfe|DLh9U`&fs6zaLX07_kZ>QIdGEdNAK$m$wZ41TE$dy21)PWTJbBJOd+)RN zZ~r!rZ`xYPNGVE*iHXTvyL!bzOl&7eOl(Kxp54F|5BcyO;Kz<|2P<>2I>NC9V6ZE| z%*ISitRX{s(?Y7UP=kze$-pwD7PjkPr$jY=7fHm}Ky(H^z8F5aC62Rt$6C)2jNclF;c^KK@h9 z22!o53OfkAm{?(1t6++Q%CSFW@z2+2gL&Z&;19one<5v8sL&9X*&b5J~j zm5CLqRm<}Gknq*@4eD#JHqdn6$%$m8PCL7SKIx{DWY?vdPXCx@cq=g*LPnnVz-SPS zyq^%lZu6x2g$>mPa*Ab~*P-)~w&vv;IYi~h5*_`VV)XDg%FV`o0la~e+HEF0#^D18 z>cdYYsD8ev$e%X4{9fd-D5?^nbmxVYA2|?c{j*)UC)IhHi5NXD^E4*}_fc^=#6a_n zwqn_=-VCW-sb3V-6Gn>Zc|z?DzFAeGgf`lc6qd!;?==w~w6L3!)eC9Xn+^^G{IEi_1`a zLK06IAMH`9G#0Y6yJHY_bIWp?iV5FSX(2t}4Lz~$;}H=38v0Wzy6wdNJ}himQ-4aK zFl&m@6S1yvGH)whfS^z`HK#S(5nRm|Q+kb~l?WNZaM>$$>NIq1WW{dQCR+6J6Q{G6 zow?iv?hwo=(oPL4+0Vq^SQ-4)8Oi$u>-YRhsmCjW1!g@kMf^U4G9eEKTU=kd946?T zZ#jYQdN5wTci~uCb!PqX*wA&I$KyGlb+b2DG$2PgodiQiG9!apAydMG-Y?djU!t#% zmn>cQi+XbW)bL6+CQg@zqer4PCb<`T>cf?}Tr01HHg3B~EorJMShdZ}PkzlF6sYH0h>gK&bd`MqDZ} z(Jkmk;iolm8-24yiF->|3?oI!7hnIpl-#uSx0-4~W3hsJmPgJyn}~OvNP8DgjmYO1 z$!fYT^&VgK|BMVi0#72B@eMHjU&i+E9`EUnk5AX;NEzP4FG7tG|Dx zz-(Qkq9)Tr3Vxj%rJ76$<3w1{OB4&!TIL1pW%0h=?VE5(_=BAzprF<&xAw%$J@dts$Z zU{^yk;||Wl^|s#U1a)q{#jOkU^4?_>P<9*9l<}OQv)=eMC$&{dPia@YZ&C?IMJ_)Jv%>Ls|%hacp-^ju?DpB z>$;k8w)ctsEPX2?QPolVbM`y2gPmW`y#}?T8JpokP1o@2eFZi1z1qiX5}HvTVGen} zE1(I6JnDEQ;r;>TcBm_DGmfW=iWg$sy;s*iNV}u(Wn=xY%Uzhv7p|)@esD3btkr81 zT?g1?k9sp6<{%*p`sGb8Be-C5W%che7F<%<3YDA4)(t@AHb$}^tp-LfQrcV`!Ir`p z5WWu88Q&=zN{^1)iI>~(*6SRFHQ^TG=&PKRkB?hBIj?WUt@}3NF(ZU_whZ-Us2y#0 zQbrEJ@Ow{LVZF^0YM;+)_xzWJ^$ov6=QNRqQ4fh&-qZS>m+PaNA&&K4KCS`ATv5Rr z9KF$_dv)x+5L^!LoTX1-6mm0yOXBnR9J#IdHG>9p_d>uMjYPA)!_fOZ^h{l_y}ZM@Pg`}axWj=l8_y^OVJi+*pKZX^YUsvV?;pp*>g zz3fVE98Bv2_jz9EX9t1T=C|a}g`|X8YxrA#BNIYHP&9^U@ zijRyNfSQODp2djTH`6QwFv7H}%XbPDo_$uBSxeTskg)a3$ZFZsBlvln9{jt=udBgb z#^D(^R>rTK97B4eXd0nET+3V=a&C>UmdBY4@BV5v__FP7ex>Z~sHq+v%D{l-5&s%W z*<4x1df+l&n%Fw>G!tMAVP#_vpRMujGaO-GJZNUHW8h0D0T@tbNLb$2^%Y9j{qvsu z>`eWKr%w&x^>&@FqJ!3U|Wjl2u> z37b%A_~4&d-=EM!n=Ds-TuvW0Vny}F*ZQ6EV+P|(!^Rl~qHxwxRk;PBf&XLV*bKEC zkEuqcl-(G78HXeksOF}?n?%Clt#~t~q3a&v@=tyx{e9l?l-(1;USFc-D}x=p)67jC zCQvfnzxW)}Bd*8%qgLL3OYTx!pt|TH zXOs21?A!V5+TbIaL77m>W3_nl(@p7bvn$tvyhSp{DUjh{ZTSKNBKMr_kDpUe0~5p9+N+*4Q9gt=4V+&?QM0GijWafg{IvB&=8j6LXZM%@BU`du=4#0p&L7J??u=Bq~p@L>&u z8aO;JHKS=V@?Tov<|NZby3O=0_?+tK03;s4@r_m|LBnuEMFaL-GS z_>fp7+92yZFu48Mys&*sBsiN|eCEejkc(&0cKmtMW0xZEL#)N(M>zWb)}iBGkeu<_ z?Uj!4?(?0qsp=h<`+_GL{ffk_3kY$TtypDX39jeFWqy0S9Y6nOpId3#6vLYf8TX1B zn&#!N-pw%HfJV*F?uTk!_rQh@tx)0kkbdQ;PfH!Fx}}>=KUQA?a!i~f7;rv$3z;q{ zH!m0dQS-T>($}fK3hlNp#dN2bm}THx=p}MFt;#+(vT&j~(6JSeX#i`imL+R8Wwicx z<^(;%WEwf5LYX7R&uLFoIahaFerUBV+L8}Z&{MbBl$HtL6OM@_m?4K4=eKwD$$>y{ASC`rrJApDS~=IJJU7dI@zs*S-8Q7_j^x2*}OHH{5ci} zq!WZt-1^+o(>nqAF>_9kyeDJr=)}2(ew9Jow`9#<#|1?xDg(H+UQK^uf{52-vQc6( zFfCfa1^`0wT6GgM&3tl_v?*G}6A55e&7uuF)^VkaVWb%W7OlXybPc}$d7?-2m0SO> zqi5BmRf$Hco_i!9>jN(4r!9G-n|w34u0s-p{E2?!m4-zL&1kcix8T{Fge^hOJ$yW(Zp4hFUm43ygS_6SAEern2`iX> z1ligc4`5)TyjN08Hj4~y<=5tnU1pUmG^5;!O7N}*gRxmd!D3TU`sx?ojLD0}+{boz zm2^Yq(W6EDODkV|dr*K<+=znWX`6F;Mar9DKc(Q)mGmC2khmp&6cKK1Hcgt^Gp}#B z@Fvq-P^qlbP2sQ6GvtV+6-kxCGGRGHiyB2k_FQyU@VsXhRkInL4@sykSjj`l*bLt4 zL~hbmiNcGy?9T2Liu$>d0ge;jM%L7GT9i;AY7?v7{{Kxk$p%d;Gt>Ly^YY>{J5o~Xj*hPr;~&G)}w5)_J`d2 zh8p@64q|I9W9x`_l$hAi#qXx!tXk|tST$}sKH}-P zCtVQ>pGgbkoaZ{V;E0Vw%intBh@xe?El>nuxlg_+CMX`UK@&```ftC;``F5D4SLA8 zmlP`C*DRu{3=u!czY%@c+`4G6F<;VipMR<&md{)S_Bh@Z2PR5c7xSb4erEMve?zdynoiR3QrJFNz-82W~7C3UtVa(SwSaW&>6Bi6~$o(E2rg_uke*< zYkiAogQa=25fQvesjrV23Vz?HgF%tZ26dCf)62I0v@$+GVKA%fms)aK-Da$*CBm(i zdZdP-v2dB(bKhyh119{cnQ$rU$;)IF^*gNEu+bSFSrC3r(5vaeD4;;tg}!cypV59< zs9%fJH0%VXWSPr!1w;qJurgebS+M!W;BIU)iW5f@nE&~in+X=lB>G2#83cX$lMw7w zrFgLG!e(2y7VHw-&qBq;n0{{(Eislz@yPk2lv5^WNAuFaID^Hvm)Dxz zmGfNf3x}1m!3<_}F9?EQLcTxG`CZn}nn%vsh0wwY+3;25#yP2Tkd4nd<_w3TPmU=b z%7$J?+Zg$A9hZ3#d~@YbPCtLgEdLTDjkq#uZ-e-)2G)W$7OSAwvB#7U16s;MhuBt# zSb6B7kUM^`dR_a9n^qHKkQ7+TkSl6#-Pjn%=Oh2xsk*1&ec2gLD5N*>YV%gJXRS*R zq*|LdBbT6m^y=wvEVMt@Z5%0aaGtZ#Y^mFvUx>;u7@2aPb%FHW>I() zO~4XSD@`*4Y2HnatlH`hFpdwmuJ>t$19ozu(X<4eTlc!6#C2sM2jQO<>UdTPlr*23 z0#0H6`~ieFSvGdU*|a34qABXzACe641Ll_n@`nC=bS}rKZWq7r1T>-J3k>6QJ@Nrgmsrxj&(uzHq^v1s59E+_iS)z`EIZ{_}HY08> ze_drK!1CTdqg0Rdn+$dfco(11;~Rd^Qhvjx?{^ zF%oXcs^*W$|4~eZG%qH}g3tVZ0DhrA721th%4*3VK)l$Gax}mMe;v}NJNhKisL0_@ z>sP&qy3CQ-Ev5@zieq8Xk3|Q2Wl4&G`u`M92isZ{oawJ2>kXLtD!&_PR%|8P8k{s~ zNW&zE)@Sk;B~_w-HXMtWzjx@?!2753x+UYdl@=uSVqn~$czS52J@jTk%ac$aa z&187ID?)loMsJd&{Jf0oP}DoN_4$S-TXrEpa6zZLTwRu{pLXfi6;`dfM-qkc;h>#$ zT>%)~qzG*$h+IiH{l9+ODMfXymqr;ZV;`P9RDfKWD8d{PE!T_RWv<{4;qzmMNLgSB z!N{^ZWGhz(s-%OXSt>yoTdTO$?t3@t$K0?I%&OXKRd_}MVeKm=>iJ^leMJcGd`F{l zf}J(wLk2vm8IZWk)c4ZxyLjQsU@MGzIKO_w?8>=OZ+A2S^GmFEzm!SKj#OMjE^txmQY0A ziT?9=%D#QpV|!N%URU#qJYG9s^)#|^nnl99FndcMTWbCW@iJfeaI#{u5@H|X^&ttZ zmS?R2Hw%A~Zosi4daq-vjJO{T&V(IJKANn~a(WClpwN=UllmO3)a0SfwSKU&KoE>6 z^{CwM17qY4T7KBDx;aDKO)cUDrqen71gWB=hWcbv>B*r1+)q0%;naq+xT|G~(R6 z{@D$*_c?F7;qAz_5X~E)v)Wf6BAH-^Xq z#5hu0SS7}R3dP7H^haeYg`oo!H49etN|SB1O8HoA#b$Fc+Qsl(DYQ$C1s6<4SvxLw zan`62mFzQBYp#_YNQxC=4~S{XYB5!wU%M>HPV(8Aj{5010%7CQyfPif8BqQ5fmKO8 z$L}s%6Wpb!qEt)E_#m7~C@7zD5h3B`GRT)kZ$}=2e6sF_#}ysbT3Vu{%F8Z4*F^jJ zCeezm1PEDh68HU0Ob~K2%d57ju8f(KVSl~Fr3>O4d)`=6OpLD!_#VASRqz8Vr(VaB z&*o-K|61QZm-|gwLD*F@UBTa7O;uX{8YF!1rQDcb`FC?qO&-`RtZ?}Bl1O1-xAUgO zW}|H#!hzpiJksS*3VEHnkeafjG+v)j33D{*R`1BNEub9E;@qp0iBNxB*YRfT#ChZ| zN>QOt>RjjJPEh3h3f^V6IH%5)|Bzd?vI8qe4RI40P#=_n|Id=knEfHN)k;!PzXc=I z*@CC?A)CE3FEk9i)JAU#%;u`@D^_;dJ=u=V(Pd)TqU{L!Pj5qYG2=oMNd_zGx6Kd4IRq zkT#$@cAN;R6|)@a8_PR-iZmZ7hphhvSNfre0Ar?K@2QICm@Zq+S~(N_O26# zMlO~2E9Qfvs?~B=e^xYE>Pn@UZzTq8sz2;#tSH#0x$)HE$C(zzBw7xcUs^7(UAu!Hge8FF}% zh$D*(s@l4#wX}~_exL7%6S*WlEYeF5z78MGl0|%IMkY~0U4v-Kkzb0Ib)TCp^FDTT zI7H^T?oT!s20aV4X@Hm=U`ri?=3UjOw3W*{nV;4O?V(m9b~c*J124c{Ttmmy zKBcHax5Wkrgdo*CC3&*fv*8>dkV-=JygnHg9kypIT1v8#{-<%pw|cV;1OCSGpn7`= zMYbN>L-!QzZTC7aOAu4*564Q|1_7!dZ=E(BLLMt=(Cp37$H)46WtXk$x z#zz#s@l9&V!LhLiN4o~=z!jPGq&Hy>tkY6r8w*afqoaA)gpg*L4f&E@Pqzb|?~6S; zQ^g<7)guo|>LL%+R__fveXZ!SGTLdLgu9jEwdJv8ELhCkjF>{7n37f43VJagebb|= zb`}}ftig2Z!{k>fi;B6;BKkg1BgxkSP%(;DO9)c$8e`&49R+YMPn{T}^Ff#gO|$|j zjg6wGBcovD%ml7N!m~J;&$P;MIs3SDHPRz-9Ez{W&n+cAkF6yU^I1vz|4%x$yaiv7 zLNH(3$1PZiZAu;lOA1#L4Q0s0i%PY;LOSxe1GR!bUSD5YCm#Hza0{d$cGKj93@G-X zW55k;=2@|dq(XEFc#5`8h-%d9Ua*)=8tR@Yq|SwL8DI4#rjl2uXJ0$G zP+p+Y%Gk~-zpJ6gAeaC|1fn`ARHq$C=9@Z~I;ssU88)s3V?j!z7s&Fb)62JZn$ypF zb$|@ZJ&ZT=8o+xu_*z3WY+7qWMUTn3mRj?42)fnFBqwf@+n>T7!?kQd%$t`enu;c) zGxn0#cxeX8hxd%&D5|+wiiO}Bwam1Nnie5%CD67Qbj%AZ2mun9;e>=J`qc*M3ImOD8zv}AI0;oRF`o$NWg%{U#6W{t z(-OI(=u2}iNxFwUm(7Oi6ND_%LvHY6Z*_iA&&SgRlX)3qE`QwMk%Do<;2dQRC1K+? z-$Pq+=}<5JjtnY6|Jt5UmA?+z_e(FOBeUj5du2B#>INQWvr7la32xfvf{uT>Z7LAAc_2}eRFHZTM8*PvX(?4nJum8LdI7Ub z5$RfJ+rD+voc%^U+1;wPnqVR*Smftw4Vji8$d^ieL6P)yJ^wg%i_4dCHRQb#In(B? z%`nH-nnp&mG7xIDZMdzrbw>o}Z++_}FQ0yW&wP(Rcz2q&9i5L(2a_I+x%DSe>{zAM zx+U>8yTm@Bf%^3AqN-iuBUJO9K$YKADsi`VvMD)!8+z5czDx1bA)qL|jr6`d0|1nc zdq6|~K!#Te|FAwz0;&EB6aG(b+J zlm7Iw?8j_PNPaB@k2BasZuK9^U3}?_{s^S*uRRNgeP!?f!rS31E_T@0f9Ft-ogrB$ z?P{oOmmpj~OsJfmR2JDvC=Q1!TG7Q3Kz>nbt6j!=FRl5>R)6OA#hDb_otNX=nANM~`a(y|Bmu`VkTNi)ZDf_4Q+IxkE6c~1@QNyD(qClCj&L~g~ z@&v&>>_!0~Vif5g-~1#jlw32sdM84CLTiT`Jt()BGcq6jIm}N0E5}4uMN|_3g!%m% z*ckEp{EO`Rm~+c*bxOf{)xEFD4kIXMLb@AFpiioh1t6qFN^m_JZ9g?u>(MhRVNxGl z*Ip)Agc>fBF8dtah`O*1O}&!Z)$zTU`f+jdJeVltDdOj^qZK#`Fu|LO^u69<{-f3s zx%R&w9aj3#=rl{iSn&{uT_|hY)#c;#D#5GLXym*Fd_J?dTBF|+w0U}T!xo7B zbK4xNWmgNIHom0)s#}d=hLP6yW7#KjJTJagZm~-x2zr}oHLcxps$nvMq#aECg81(r zCjh}5R$gJpU^etj{HLUJNTvQzDGN4Hlq*a0sw^#cl2I25_W7Uw^{~ z0TF8AYQTgignlvpPOjP%<&`--y5^jvy6>u25nx9Oh#DA*`7XF^fF@fSHoLyoe;5h? zaRvOc2HEhJU({aU1)h4#MrM!0VhV^En(pa>4%5BLm~HB8SuE1Myer?)}$6lahz* zc&&UVH^z|WIQT|x1~@<$T~M!T7ZS_5HvuZ<#+MI`>Ez=@EioH`n=9EiB@B>v)zGOx z&%{1(hZ@!YY*6)@zP-mf{S8#LuctQ~-lJnX%)N9r%p(hVwTNLTbAt;z8(Xu|`t*RR zFRs>s?LUG#eBBUlD1-^D{OaX&*?s9sJ$RI`>?61^sVb}V{FwD0VLl||n4pmOsZM!YQ;55M@@ zlnA?7b)9qh1-jCz7A+(X!pN(Z!sE=5KHjYqH==sPnk{_N49y6}`1xYy*4p(e)_$UI z%u!Tn)w8i*47o!BxHfhyPhs5)^w*#JpIWpBOjElwp^8eVtspr za7YE)Dwd(sjHI}-1ptcO1@;zWDOjPtlFFKQP--- zEDjqnYY2RTxTu4cQ}FKUiuS~Bz8o@{%L!&F)t3~&hc_0&Cd1U3&As#uKNbWi8-dqI zv#R|>n?du0)^(G8YZLyoTN^F{bE@aaEpFHWj-VJhKVf`L!(`M9{=g~Dz0kgt;KA*O z*Hi#?7L1$--q5dGJ9R%BoKSvT_Ci(fW=Qz4D>avEKbNmn+SYUbFyC^wa^)G!J~|PL zferP8M`IV^mw3v-CMH}=*jAL`H0TKs?zO4uo=~#}0owwzAEBt@xfNu{YMzJW^?|c-G|KfAsDpf;G@WLYc99c5q0CBLDBJYVJw z#^v9$TOGcgB=Q|{>Z}s#SSx2bc~lw!w)SSFNG;!`YjZ7(Jg>jZzSCd2wqoy=SEVuH zUCATn^T-q2(Eh3|kHrRfutA@5q^$LgWB!MEs=?uYaTlp-SyXoJ9P3iLZ5`2+kWWr5R@YQS z4V*21F&(s9tm2`mHwgQ*SPzA>CZ_^`;HllT z$*1Sxk~UBW|M~wqa_PUs)LMYpsb1gp{9hiynV{KT`Ts942>w@S4hN+{+m-32obE-x zcJSI?Nlicyhv+|2o1%)+rv}$|Hv-YrDZ?>5-E-P}Dx7FEk%L<<-R2kUgMBAN;j{%n zmAzn_A24;{uOTMtn1q2pdy_;{)YY&F4Hgq49Xluk+OExZWPm}7+n8tT_69z^uLuWf z6CO8D!w(w-1drFrQ1OH3JMV7SrXzdhUG7>bBb-)JUAle|P{wh(P8IF*#c7Q@?Ew_W>hGv`&gbgOkTs{l%aq@xmg;~;YA2Ct?g z;Z#v&qW@uR+BQLgd7GJdO7`s7<5p^Dh$5mcw2!jLw zFS&Pvng?L)ivU5u=ViGK13Oh_uSB7ta6y-o>k~wb1X-77PJ=m`#Qa!hrIzs54}#$| z>Mytr1(PkHF8k~F3>rIu##|#cw%7?bVh?W$!&t=fwJt8~E}035^UkoDxKzIWI&P`` zrY3 zZJE`Cv{3e|2ml<(IeWWiJ1e=HU)xz*LLs!?SguzIZmXL&1egjlw_3JqRTG$~>7$|Y zO|oB?IyowJYx51_&&@}GpnfC!xNiDNrg_F(X-v?xd)ZtyM*y7wO2-~Ywq7{ezJgMF z1%xjvYqC+y=x_aneJvzkB9&Vl2#SB+yjV=YC-3(#5eBnDGlU(%o$LmiV5$l>*+h^3 znjks6i~<`Nhu}8G>n|HZYEw)S0D4NF4{#`huZ9KKlGkLNk3kJhG+CbOgq$Id1i^1w zG94GSQ%!wm!$x*9Lr}at+P)$yUHJ6t?rj!|ts(aV&hL>8-QW%CQpW*b;LH>*HW42- zuG^V2k31V`@S#=&IuKc$wdiZgC7ET2Z)WDpSQ-sQbpX}bsM@LHl=+^UU2iB9;`kP9 z6NE!M&guH51o_b8YhGX_?`x#y9$ z<{Ro>!)}=_hJ9n*Wd#uh2yCP9h@CoRZkt!py+Y#U_F?U-hH8xHUzLW&3Bzqa-pq%Y z*eB=0II|QTgKK&byJ&DTEJc0v(!umd$|qEAKs>pfDn9v`h-w60XW&p93|S`Ko7Zxgm8~kR@w# zf{4l^rV%I62qO;mq~G6;x+eSiv`GL&|KH)|U_IrEvW<^o;zyT`g~(?l-iG|mrhQ0B7K zT^?5J3}$OcyO;o($9`P@FC9iUJpc?Wb<#=|89{U2m8=5moC_23$5YtDz7j77HSvb= zs+QQaZh*G&2}ms!%Ix0`f5&fAOSp32`NGN50x7Q>+2{5aF&-*~?xE!kD}2z=Og@6d z`ri^yXTxcy6vxe2|&Bq{B2LaWq4&p<$1NVeDZxAh>fqZLwY%4Jtbi+_^;Vk!s_Z%D8b?-E~5;tJ!B9nna(OtUrkA{U3Tp`J(UmmXrohyp;~TZW@v+# zFUxfu3b{q)k0bW2Y3%||kY9J@KKzCBqDlQ2Y<~VA0414b?uRO-nq~&<=%cBOX4@KX zgGivG|3x0*e+eS;zus!_7o|@gn5-4JEBEX*i3c4DKL#iEf&wsz!~gcW*pGZjQwKVE z09MubJOEv4XQ=!KRkPx;f(ulvd4M@u-3hqr-%bHR9aMh1U1$jC zAtI@5M>e_1raKFrx1|b_PTXx;4aDkV(8EA_ed{<70g}!Eb85F6jtbp?Iqtw@G1Des zPBVZ+iv8vf%=y3nfqu+Hf8hpy!q$L&=z797;i7S;tp3k0M7oTE1k}oV1)LW3KhUrg z|A}ob&O<@WpTJpozX#@~=wBN4TgP=}GB?JTk!i#s9?bJ*JY5T8Prl_{-J-Pn)6)`g z^ON#Q!~tT@K5#!W5G8H)W5NEXNEJy3qN?J{|BPy$B|PCQR$RUt(uVje#0_<oqql4{8s#}0ZTvSB10t9c z{C>#)qIRM`r+f80`_M$R`mLHh%1X5Tjk1)w&Ls+v2?gcWBXA)q(hIwGG@U_M3%>oe zr5hRg<IZHcx9q}&S>dffJ&stfW zU)TM1*gOT73Ew)En-~{_Y-$tqPwXQ{M4tD$|MBDuZOxOsxs0`OIZ67!!1QV6+S1Di zVcWuZG~`scpY>-&O}H=p%RO}j^aEHo($5Op<9ZsPsL^t*?E9rEsz)xclOAB~4)43N z!<3O`oE4dR@id;wk2#bK~A^@qlO@+@pX zr-HS|+=jz(j(&7k$^NLw8ts4#VKcY!w~n-oz_<_wny>>Sjialk>>Sogn0`aJtVFa6 zX5>|!0f2d8-?Qj$VT_uts>iJ^PY*~QP3s>~0BJlI(`b{Yn~Z+K`SghM^Ev^2Jt6pI zdVuoDyn4htCse!X)Eji5!AiQ^>8dr)0|o{0KQ)m4Vlaq0zVvld-@)&^5gw`y6BkM7 zo7Ob{y!DppG+Y$es zeU%O`Z&tj__RTPA#h%}eo8wHT0Y^^-DhJI2K=hN<_iA??VD3*Xx7~vC&UCr+sI}($ z5r7bFxcwlvtA)c`8N9lv?&{Uobx$i68$u1<9w&#&^jsbfQO z88=w-J5x{z; zn4N1Ze|u3Z5+VhQ{??Gm9OT zW`Ip|qmAQVWQ}RdI;^;ox5=qKZKCP6FkkpYt}M`$x$R=Jk|*zH&+fE27P%RpI#Bj_ z=aYhZIeBh}%t7d8+s5kmK~1@YVB=Q9+9c5}64&J+K)FY3``L?+P3Z+nAl2bzMHjR8 z53G)eB%_tB*kuL-%AD-3fy~D z?Tu^iOpoNx>NXtt+B`4kYgwwA-VY&?CK+ljalqT1{PA{`rYA&^=OJOP`&HEznU7ss zLq(UWqg6!Lp8%c{ej{k&KO_(69T0S&wm1C0cwpTArIjkgPT|Y8kNF=5jP9oGlHKGhDG=`JOZ$V72uvF@#rQ z7XSy(q8EkgOCR&|rOlo9LvQ}Ro!IsznmQH-CiWeR;wF7G4d?PRQf4BZ=M3;Dkf7SG zR(hrku+`LmmI(TVVs24!nT+d$oUDf1;5NJ2d5$4F%eng>ooSrtK zQI4Owi%G6gLP1MWQfUI;wFhxy4DY#db44|0^4MI%DtmCb$xxTm0(kzfQZZ9 z2UL~3R$W&D1XBmf$XzX=?)h?{R+*H9sBlhp{c3I7>9`11Q@7SPG<`{`lo7cxyUD&y zA#82T=ybE!Kf-xFzuY!7z770TPK(YvozXM5&%}zELZCB4k2hH6=`E zt3_$+Y4_dp(3%cd@XDg+k>Rc_(Oc@}a^%gzc6ws@``9xdYStDmcUP+btsRWLtG~nA zW(ct35obWIKgxWup~}sKexk(DcE8Z6W)=7PjDm)RC?%W|@XRuSNciSl6@hhDtJ%`>d z0M&Xeewq$O{aW#Q)vVU-3^_vq?wjk5fSeM}K9?I2{IU?QH$jDGJI(3T@i8lMuqTYH z&Y(EC2&NCz8GfWw^JQHA zh%X!lpDFa}EFu@BV|NS%0BKRWL~iujo9zlVGPi7I?9;+xHos0iwSLTYq2Akx;Bn8nfVy@l*{Lrr(>E;9IQI z%K>g@!$SO#Zy7q62TyS)-ayVezCGivK;M}zo`YJqrYV2cJ$o!ri7z}R9GEa;Oe?$} zop3RJ-W_tD6xcTA;qm~KU+0g(Gl$g6{L(g=SC}$WR1JdkgN?p?fDS_ z2YM9_J3q;sd+-SPSY%P)p5wLBNq~7v>32LRp}rqk_`K?}n5~Uq3N!Z3-w-i8`Bz`o zog+cPf-sma{cHby+V}^DDz0FN;l2NQF}@ zhwNM)EejPM?@WJ>L4nLn^8ZShf5(v)*o$S=ZLGme<6IvN$7r*)PW-dW`xDFd&fe4= zjQ?q8)yrgc?T7#RRm=GokdVDD5%Gbp@+GpMG`AeHJ)jEhe{+IY`#p=y$n%Gr%yeXa zJm-Iy9GsN}5nDP`V&{)g&>D`ih((c6%(fBQe@K_<{K z52Sa_r|{z<)--{Rl3ycMcu5s`(^hj!Fgu!>KRhQLjt2 znnI^KJj=Zhz4CZ#G{lKH$pH|?6LO&3g1PGT@_f~(N?)&$_Vd!9Te*=ViI#((FQ!ac zAJ8$WkB|2okXZ0IjW{M_;lYHF{K}il-xRKs0ljlUra^ z?_g!*T{fzPZRs5xqbX)9>N&T{R{r(Zc>7|I`F2qw_^9*Fytx4K`uD-BE#(DP_JPXh z&QxM_YT??_IoQd@RxEs0SA%kXNPC(P{gT$|Lpe9v*UG65HL$Sf*EC4{i@FC@WnCP^ zzP8aOeUZ;CF1)RLnWa^Au##|h(thL{;I0B3OY!uW%H^oZVZI+`zL+r>^mRZwj+N;i zX&pWHho< z`&$@}Fujnx(mBU5%R?4A1f5Sr$H8mKe-x`RYpCBwzR&tpUaw7| z8Oyl5EK4mrWHH+GukzKcxe#i|LCRMUhlS2k z>T%O*qlwKiy=35c=WU13f#r_GKwNGDbG`AphKV<)@$*G1W$3wxHH5>2vz!UcUPcPU z{T8j9s@5W>gP}nz1jPf9>kIs-V=rnS!nE_o0eDWnOxUM)>vunB?nsBjq0!g>q3D21 z|Bi7*pxJN}ThB_uTh6T$3cWBAAq7&zp3$=ZPaorpv0{BFdtg@OfSHN* zuKXOXSYAO79Moi;DObz4WtG}3500IzQv>?a_EB(c>}6LVN)mqaxz4lE(96e;bu$}5 zXj;mcKTc-wypz;^xfjD=o^t>QdSW4aE8%la{MJ&){`om5x z(G6Vu5;g-=KaRfHGRaLl-%-K2mEYD=E?WZ5L>*)O?S6VV2S4Zety}~L}lSWjg=GcQcK9uSZlWL$z zVziLm=;w5)&&iqcX(h#IEHO_ZAnX%!3~n zZvJzj2>4@#|G%?=-_KOJ{V@GqirXGA=+-|T@W(T9o&Wn5K|F)NHjP?I_ h?&2EXb7gDCu5S+}9pm+qw-Nhm7PeRF%x^#V-vD02p$Y&1 diff --git a/tutorials/create-first-ml-experiment/tutorial-1st-experiment-sdk-train.ipynb b/tutorials/create-first-ml-experiment/tutorial-1st-experiment-sdk-train.ipynb index 167a935d..09ac2ca5 100644 --- a/tutorials/create-first-ml-experiment/tutorial-1st-experiment-sdk-train.ipynb +++ b/tutorials/create-first-ml-experiment/tutorial-1st-experiment-sdk-train.ipynb @@ -31,7 +31,7 @@ "\n", "> * Connect your workspace and create an experiment \n", "> * Load data and train a scikit-learn model\n", - "> * View training results in the portal\n", + "> * View training results in the studio\n", "> * Retrieve the best model" ] }, @@ -74,7 +74,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Now create an experiment in your workspace. An experiment is another foundational cloud resource that represents a collection of trials (individual model runs). In this tutorial you use the experiment to create runs and track your model training in the Azure Portal. Parameters include your workspace reference, and a string name for the experiment." + "Now create an experiment in your workspace. An experiment is another foundational cloud resource that represents a collection of trials (individual model runs). In this tutorial you use the experiment to create runs and track your model training in the Azure Machine Learning studio. Parameters include your workspace reference, and a string name for the experiment." ] }, { @@ -171,7 +171,7 @@ "\n", "1. For each alpha hyperparameter value in the `alphas` array, a new run is created within the experiment. The alpha value is logged to differentiate between each run.\n", "1. In each run, a Ridge model is instantiated, trained, and used to run predictions. The root-mean-squared-error is calculated for the actual versus predicted values, and then logged to the run. At this point the run has metadata attached for both the alpha value and the rmse accuracy.\n", - "1. Next, the model for each run is serialized and uploaded to the run. This allows you to download the model file from the run in the portal.\n", + "1. Next, the model for each run is serialized and uploaded to the run. This allows you to download the model file from the run in the studio.\n", "1. At the end of each iteration the run is completed by calling `run.complete()`.\n", "\n" ] @@ -180,7 +180,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "After the training has completed, call the `experiment` variable to fetch a link to the experiment in the portal." + "After the training has completed, call the `experiment` variable to fetch a link to the experiment in the studio." ] }, { @@ -196,14 +196,14 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## View training results in portal" + "## View training results in studio" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Following the **Link to Azure Portal** takes you to the main experiment page. Here you see all the individual runs in the experiment. Any custom-logged values (`alpha_value` and `rmse`, in this case) become fields for each run, and also become available for the charts and tiles at the top of the experiment page. To add a logged metric to a chart or tile, hover over it, click the edit button, and find your custom-logged metric.\n", + "Following the **Link to Azure Machine Learning studio** takes you to the main experiment page. Here you see all the individual runs in the experiment. Any custom-logged values (`alpha_value` and `rmse`, in this case) become fields for each run, and also become available for the charts and tiles at the top of the experiment page. To add a logged metric to a chart or tile, hover over it, click the edit button, and find your custom-logged metric.\n", "\n", "When training models at scale over hundreds and thousands of runs, this page makes it easy to see every model you trained, specifically how they were trained, and how your unique metrics have changed over time." ] @@ -212,21 +212,21 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "![Main Experiment page in Portal](imgs/experiment_main.png)" + "![Main Experiment page in the studio](../imgs/experiment_main.png)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Clicking on a run number link in the `RUN NUMBER` column takes you to the page for each individual run. The default tab **Details** shows you more-detailed information on each run. Navigate to the **Outputs** tab, and you see the `.pkl` file for the model that was uploaded to the run during each training iteration. Here you can download the model file, rather than having to retrain it manually." + "Select a run number link in the `RUN NUMBER` column to see the page for an individual run. The default tab **Details** shows you more-detailed information on each run. Navigate to the **Outputs + logs** tab, and you see the `.pkl` file for the model that was uploaded to the run during each training iteration. Here you can download the model file, rather than having to retrain it manually." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "![Run details page in Portal](imgs/model_download.png)" + "![Run details page in the studio](../imgs/model_download.png)" ] }, { @@ -240,7 +240,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "In addition to being able to download model files from the experiment in the portal, you can also download them programmatically. The following code iterates through each run in the experiment, and accesses both the logged run metrics and the run details (which contains the run_id). This keeps track of the best run, in this case the run with the lowest root-mean-squared-error." + "In addition to being able to download model files from the experiment in the studio, you can also download them programmatically. The following code iterates through each run in the experiment, and accesses both the logged run metrics and the run details (which contains the run_id). This keeps track of the best run, in this case the run with the lowest root-mean-squared-error." ] }, { @@ -352,7 +352,7 @@ "\n", "> * Connected your workspace and created an experiment\n", "> * Loaded data and trained scikit-learn models\n", - "> * Viewed training results in the portal and retrieved models\n", + "> * Viewed training results in the studio and retrieved models\n", "\n", "[Deploy your model](https://docs.microsoft.com/azure/machine-learning/service/tutorial-deploy-models-with-aml) with Azure Machine Learning.\n", "Learn how to develop [automated machine learning](https://docs.microsoft.com/azure/machine-learning/service/tutorial-auto-train-models) experiments." diff --git a/tutorials/image-classification-mnist-data/img-classification-part2-deploy.ipynb b/tutorials/image-classification-mnist-data/img-classification-part2-deploy.ipynb index de8090f3..f1d54c4c 100644 --- a/tutorials/image-classification-mnist-data/img-classification-part2-deploy.ipynb +++ b/tutorials/image-classification-mnist-data/img-classification-part2-deploy.ipynb @@ -39,11 +39,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "tags": [ - "register model from file" - ] - }, + "metadata": {}, "outputs": [], "source": [ "# If you did NOT complete the tutorial, you can instead run this cell \n", @@ -62,7 +58,19 @@ " model_name=model_name,\n", " tags={\"data\": \"mnist\", \"model\": \"classification\"},\n", " description=\"Mnist handwriting recognition\",\n", - " workspace=ws)" + " workspace=ws)\n", + "\n", + "from azureml.core.environment import Environment\n", + "from azureml.core.conda_dependencies import CondaDependencies\n", + "\n", + "# to install required packages\n", + "env = Environment('tutorial-env')\n", + "cd = CondaDependencies.create(pip_packages=['azureml-dataprep[pandas,fuse]>=1.1.14', 'azureml-defaults'], conda_packages = ['scikit-learn==0.22.1'])\n", + "\n", + "env.python.conda_dependencies = cd\n", + "\n", + "# Register environment to re-use later\n", + "env.register(workspace = ws)" ] }, { @@ -98,190 +106,16 @@ "print(\"Azure ML SDK Version: \", azureml.core.VERSION)" ] }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Retrieve the model\n", - "\n", - "You registered a model in your workspace in the previous tutorial. Now, load this workspace and download the model to your local directory." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "tags": [ - "load workspace", - "download model" - ] - }, - "outputs": [], - "source": [ - "from azureml.core import Workspace\n", - "from azureml.core.model import Model\n", - "import os \n", - "ws = Workspace.from_config()\n", - "model=Model(ws, 'sklearn_mnist')\n", - "\n", - "model.download(target_dir=os.getcwd(), exist_ok=True)\n", - "\n", - "# verify the downloaded model file\n", - "file_path = os.path.join(os.getcwd(), \"sklearn_mnist_model.pkl\")\n", - "\n", - "os.stat(file_path)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Test model locally\n", - "\n", - "Before deploying, make sure your model is working locally by:\n", - "* Downloading the test data if you haven't already\n", - "* Loading test data\n", - "* Predicting test data\n", - "* Examining the confusion matrix" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Download test data\n", - "If you haven't already, download the test data to the **./data/** directory" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from azureml.core import Dataset\n", - "from azureml.opendatasets import MNIST\n", - "\n", - "data_folder = os.path.join(os.getcwd(), 'data')\n", - "os.makedirs(data_folder, exist_ok=True)\n", - "\n", - "mnist_file_dataset = MNIST.get_file_dataset()\n", - "mnist_file_dataset.download(data_folder, overwrite=True)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Load test data\n", - "\n", - "Load the test data from the **./data/** directory created during the training tutorial." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from utils import load_data\n", - "import os\n", - "\n", - "data_folder = os.path.join(os.getcwd(), 'data')\n", - "# note we also shrink the intensity values (X) from 0-255 to 0-1. This helps the neural network converge faster\n", - "X_test = load_data(os.path.join(data_folder, 't10k-images-idx3-ubyte.gz'), False) / 255.0\n", - "y_test = load_data(os.path.join(data_folder, 't10k-labels-idx1-ubyte.gz'), True).reshape(-1)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Predict test data\n", - "\n", - "Feed the test dataset to the model to get predictions." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import pickle\n", - "import joblib\n", - "\n", - "clf = joblib.load( os.path.join(os.getcwd(), 'sklearn_mnist_model.pkl'))\n", - "y_hat = clf.predict(X_test)\n", - "print(y_hat)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Examine the confusion matrix\n", - "\n", - "Generate a confusion matrix to see how many samples from the test set are classified correctly. Notice the mis-classified value for the incorrect predictions." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from sklearn.metrics import confusion_matrix\n", - "\n", - "conf_mx = confusion_matrix(y_test, y_hat)\n", - "print(conf_mx)\n", - "print('Overall accuracy:', np.average(y_hat == y_test))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Use `matplotlib` to display the confusion matrix as a graph. In this graph, the X axis represents the actual values, and the Y axis represents the predicted values. The color in each grid represents the error rate. The lighter the color, the higher the error rate is. For example, many 5's are mis-classified as 3's. Hence you see a bright grid at (5,3)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# normalize the diagonal cells so that they don't overpower the rest of the cells when visualized\n", - "row_sums = conf_mx.sum(axis=1, keepdims=True)\n", - "norm_conf_mx = conf_mx / row_sums\n", - "np.fill_diagonal(norm_conf_mx, 0)\n", - "\n", - "fig = plt.figure(figsize=(8,5))\n", - "ax = fig.add_subplot(111)\n", - "cax = ax.matshow(norm_conf_mx, cmap=plt.cm.bone)\n", - "ticks = np.arange(0, 10, 1)\n", - "ax.set_xticks(ticks)\n", - "ax.set_yticks(ticks)\n", - "ax.set_xticklabels(ticks)\n", - "ax.set_yticklabels(ticks)\n", - "fig.colorbar(cax)\n", - "plt.ylabel('true labels', fontsize=14)\n", - "plt.xlabel('predicted values', fontsize=14)\n", - "plt.savefig('conf.png')\n", - "plt.show()" - ] - }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Deploy as web service\n", "\n", - "Once you've tested the model and are satisfied with the results, deploy the model as a web service hosted in ACI. \n", + "Deploy the model as a web service hosted in ACI. \n", "\n", "To build the correct environment for ACI, provide the following:\n", "* A scoring script to show how to use the model\n", - "* An environment file to show what packages need to be installed\n", "* A configuration file to build the ACI\n", "* The model you trained before\n", "\n", @@ -324,52 +158,6 @@ " return y_hat.tolist()" ] }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Create environment file\n", - "\n", - "Next, create an environment file, called myenv.yml, that specifies all of the script's package dependencies. This file is used to ensure that all of those dependencies are installed in the Docker image. This model needs `scikit-learn` and `azureml-sdk`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "tags": [ - "set conda dependencies" - ] - }, - "outputs": [], - "source": [ - "from azureml.core.conda_dependencies import CondaDependencies \n", - "\n", - "myenv = CondaDependencies()\n", - "myenv.add_conda_package(\"scikit-learn==0.22.1\")\n", - "myenv.add_pip_package(\"azureml-defaults\")\n", - "\n", - "with open(\"myenv.yml\",\"w\") as f:\n", - " f.write(myenv.serialize_to_string())" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Review the content of the `myenv.yml` file." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "with open(\"myenv.yml\",\"r\") as f:\n", - " print(f.read())" - ] - }, { "cell_type": "markdown", "metadata": {}, @@ -432,6 +220,11 @@ "from azureml.core.webservice import Webservice\n", "from azureml.core.model import InferenceConfig\n", "from azureml.core.environment import Environment\n", + "from azureml.core import Workspace\n", + "from azureml.core.model import Model\n", + "\n", + "ws = Workspace.from_config()\n", + "model = Model(ws, 'sklearn_mnist')\n", "\n", "\n", "myenv = Environment.get(workspace=ws, name=\"tutorial-env\", version=\"1\")\n", @@ -470,14 +263,148 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Test deployed service\n", + "## Test the model\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Download test data\n", + "Download the test data to the **./data/** directory" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "from azureml.core import Dataset\n", + "from azureml.opendatasets import MNIST\n", + "\n", + "data_folder = os.path.join(os.getcwd(), 'data')\n", + "os.makedirs(data_folder, exist_ok=True)\n", + "\n", + "mnist_file_dataset = MNIST.get_file_dataset()\n", + "mnist_file_dataset.download(data_folder, overwrite=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Load test data\n", + "\n", + "Load the test data from the **./data/** directory created during the training tutorial." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from utils import load_data\n", + "import os\n", + "\n", + "data_folder = os.path.join(os.getcwd(), 'data')\n", + "# note we also shrink the intensity values (X) from 0-255 to 0-1. This helps the neural network converge faster\n", + "X_test = load_data(os.path.join(data_folder, 't10k-images-idx3-ubyte.gz'), False) / 255.0\n", + "y_test = load_data(os.path.join(data_folder, 't10k-labels-idx1-ubyte.gz'), True).reshape(-1)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Predict test data\n", + "\n", + "Feed the test dataset to the model to get predictions.\n", "\n", - "Earlier you scored all the test data with the local version of the model. Now, you can test the deployed model with a random sample of 30 images from the test data. \n", "\n", "The following code goes through these steps:\n", "1. Send the data as a JSON array to the web service hosted in ACI. \n", "\n", - "1. Use the SDK's `run` API to invoke the service. You can also make raw calls using any HTTP tool such as curl.\n", + "1. Use the SDK's `run` API to invoke the service. You can also make raw calls using any HTTP tool such as curl." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import json\n", + "test = json.dumps({\"data\": X_test.tolist()})\n", + "test = bytes(test, encoding='utf8')\n", + "y_hat = service.run(input_data=test)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Examine the confusion matrix\n", + "\n", + "Generate a confusion matrix to see how many samples from the test set are classified correctly. Notice the mis-classified value for the incorrect predictions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from sklearn.metrics import confusion_matrix\n", + "\n", + "conf_mx = confusion_matrix(y_test, y_hat)\n", + "print(conf_mx)\n", + "print('Overall accuracy:', np.average(y_hat == y_test))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Use `matplotlib` to display the confusion matrix as a graph. In this graph, the X axis represents the actual values, and the Y axis represents the predicted values. The color in each grid represents the error rate. The lighter the color, the higher the error rate is. For example, many 5's are mis-classified as 3's. Hence you see a bright grid at (5,3)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# normalize the diagonal cells so that they don't overpower the rest of the cells when visualized\n", + "row_sums = conf_mx.sum(axis=1, keepdims=True)\n", + "norm_conf_mx = conf_mx / row_sums\n", + "np.fill_diagonal(norm_conf_mx, 0)\n", + "\n", + "fig = plt.figure(figsize=(8,5))\n", + "ax = fig.add_subplot(111)\n", + "cax = ax.matshow(norm_conf_mx, cmap=plt.cm.bone)\n", + "ticks = np.arange(0, 10, 1)\n", + "ax.set_xticks(ticks)\n", + "ax.set_yticks(ticks)\n", + "ax.set_xticklabels(ticks)\n", + "ax.set_yticklabels(ticks)\n", + "fig.colorbar(cax)\n", + "plt.ylabel('true labels', fontsize=14)\n", + "plt.xlabel('predicted values', fontsize=14)\n", + "plt.savefig('conf.png')\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Show predictions\n", + "\n", + "Test the deployed model with a random sample of 30 images from the test data. \n", + "\n", "\n", "1. Print the returned predictions and plot them along with the input images. Red font and inverse image (white on black) is used to highlight the misclassified samples. \n", "\n",