{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# BOA318: Build a fitness activity tracker using machine learning\n", "## re:Invent 2022\n", "\n", "![title](img/app-in-action.jpg)\n", "\n", "In this notebook we will use Amazon SageMaker to: \n", "- Create a data transformer\n", "- Train the data transformer on our dataset\n", "- Create a ML model\n", "- Train the ML model \n", "- Create a pipeline model to chain together the transformer and the ML model\n", "- Host the pipeline model ready to make predictions from our app " ] }, { "cell_type": "markdown", "id": "fe1f71cb", "metadata": {}, "source": [ "Before we get started, we will load some of the Python libraries we need:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import os\n", "import boto3\n", "\n", "import sagemaker\n", "\n", "from sagemaker import image_uris\n", "from sagemaker.session import Session\n", "from sagemaker.inputs import TrainingInput\n", "\n", "from sagemaker.model import Model\n", "from sagemaker.pipeline import PipelineModel\n", "\n", "from sagemaker.sklearn.estimator import SKLearn\n", "\n", "import pandas as pd" ] }, { "cell_type": "markdown", "id": "ba8cd181", "metadata": {}, "source": [ "Now we will set up our Amazon SageMaker working environment. This includes the session object that will be used by the SageMaker SDK in this notebook, and an S3 location for SageMaker to store assets it's working on:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "sagemaker_session = sagemaker.Session()\n", "role = sagemaker.get_execution_role()\n", "\n", "bucket = sagemaker_session.default_bucket()\n", "prefix = \"activity-tracker-pipline-workshop\"" ] }, { "cell_type": "markdown", "id": "1e7c7829", "metadata": {}, "source": [ "## Send our data to S3 (SageMaker)\n", "\n", "This handy snippet of code will take the training data we have and use the SageMaker SDK to upload it to S3:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "for path, subdirs, files in os.walk(\"training_data\"):\n", " for name in files:\n", " if ('.ipynb_checkpoints' not in path) and (name.endswith(\".csv\")): \n", " csv_data_file = os.path.join(path, name)\n", " \n", "train_input = sagemaker_session.upload_data(\n", " path=csv_data_file,\n", " bucket=bucket,\n", " key_prefix=\"{}/{}\".format(prefix, \"train\"),\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## The Data Transformer\n", "\n", "We will now see how we can use scikit-learn to apply some processing to our data.\n", "\n", "We are creating a SageMaker data transformer that will:\n", "- Fill in missing data with scikit-learn SimpleImputer (Not that we have any missing data at this time)\n", "- Standardize features by removing the mean and scaling to unit variance with scikit-learn StandardScaler\n", "\n", "The transformer is defined in a Python file `./scripts/pre_processor_script.py`. Once we create the transformer we will train it on our training data, that way it will be able to apply the same pre-processing when we train our ML model AND on new data that gets sent to our endpoint once it's trained.\n", "\n", "Take a look at the code, there's no need to change anything: `./scripts/pre_processor_script.py`" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Create the Transformer\n", "\n", "To create the transformer, we specify the location of our script, and the infrastructure parameters we want to use." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "FRAMEWORK_VERSION = \"1.0-1\"\n", "script_path = \"scripts/pre_processor_script.py\"\n", "\n", "sklearn_preprocessor = SKLearn(\n", " entry_point=script_path,\n", " role=role,\n", " framework_version=FRAMEWORK_VERSION,\n", " instance_type=\"ml.c4.xlarge\",\n", " sagemaker_session=sagemaker_session,\n", " environment={\"SAGEMAKER_DEFAULT_INVOCATIONS_ACCEPT\": \"text/csv\"}\n", ")" ] }, { "cell_type": "markdown", "id": "e03ea3d3", "metadata": {}, "source": [ "Now we train the transformer:\n", "\n", "*(Only run this once. If you're trying to reconnect to the notebook, see the next two cells instead.)*" ] }, { "cell_type": "markdown", "id": "a9697fa1", "metadata": {}, "source": [ "![title](img/wait_start.png)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Only run this once. If you're trying to reconnect to the notebook, use the next two cells instead.\n", "\n", "sklearn_preprocessor.fit({\"train\": train_input})" ] }, { "cell_type": "markdown", "id": "c13d3bbc", "metadata": {}, "source": [ "![title](img/wait_end.png)" ] }, { "cell_type": "markdown", "id": "bd1d0e07", "metadata": {}, "source": [ "### Need Help? Oh no! I closed the notebook or somehow lost connection and I don't want to train that again....\n", "\n", "Use the following two cells to get back on track. Ask for help if you need. :) " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# echo \"Look for the last run job prefixed sagemaker-scikit-learn-...\"\n", "# !aws sagemaker list-training-jobs" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# # Paste the name of the last run job from above into this line...\n", "# sklearn_preprocessor = sklearn_preprocessor.attach('sagemaker-scikit-learn-xxxxxxxxxxxxxxxxxx', sagemaker_session)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Preprocess Training Data with the Transformer\n", "\n", "Now we have a transformer that's trained on our data. Next we use it to process that same data, and create a processed dataset.\n", "\n", "Here we ask Amazon SageMaker to spin up some compute infrastructure for us and handle all the heavy lifting. \n", "\n", "First we define the infrastructure we want:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "transformer = sklearn_preprocessor.transformer(\n", " instance_count=1, \n", " instance_type=\"ml.m5.xlarge\", \n", " assemble_with=\"Line\", \n", " accept=\"text/csv\",\n", ")" ] }, { "cell_type": "markdown", "id": "557b839b", "metadata": {}, "source": [ "Now we ask Amazon SageMaker to start the processing:\n", "\n", "*(Only run this once. If you're trying to reconnect to the notebook, see the next two cells instead.)*" ] }, { "cell_type": "markdown", "id": "a9697fa1", "metadata": {}, "source": [ "![title](img/wait_start.png)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Only run this once. If you're trying to reconnect to the notebook, use the next two cells instead.\n", "\n", "transformer.transform(train_input, content_type=\"text/csv\")\n", "print(\"Waiting for transform job: \" + transformer.latest_transform_job.job_name)\n", "transformer.wait()\n", "preprocessed_train = transformer.output_path" ] }, { "cell_type": "markdown", "id": "c13d3bbc", "metadata": {}, "source": [ "![title](img/wait_end.png)" ] }, { "cell_type": "markdown", "id": "c0c322fa", "metadata": {}, "source": [ "### Need Help? Oh no! I closed the notebook or somehow lost connection and I don't want to train that again....\n", "\n", "Use the following two cells to get back on track. Ask for help if you need. :) " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# echo \"Look for the last run job prefixed sagemaker-scikit-learn-...\"\n", "# !aws sagemaker list-transform-jobs" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# # Paste the name of the last run transform job from above into this line...\n", "# transformer = transformer.attach('sagemaker-scikit-learn-xxxxxxxxxxxxxxxxxxxxxxxx', sagemaker_session)\n", "# preprocessed_train = transformer.output_path" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## The Machine Learning Model \n", "\n", "As you have seen, most of the battle in ML is getting the data ready! But now we are ready to start working with a machine learning model. \n", "\n", "From the analysis we performed in SageMaker Data Wrangler, we saw that the problem space is quite simple and a simple tree based algorithm will perform very well. For this notebook we will use Amazon SageMaker's built in XGBoost algorithm.\n", "\n", "The [XGBoost (eXtreme Gradient Boosting)](https://docs.aws.amazon.com/sagemaker/latest/dg/xgboost.html) algorithm is a popular and efficient open-source implementation of the gradient boosted trees algorithm.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Defining the Model\n", "\n", "Here we set some hyperparameters for the algorithm.\n", "\n", "You will see the hyperparameters are very specific. Hmmm, how did we know these exact values? Earlier I used Amazon SageMaker AutoML to create some candidate models and automaticaly work out some good hyperparameter values. If you're interested in having a go take a look here: [Amazon SageMaker Autopilot](https://aws.amazon.com/sagemaker/autopilot)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "hyperparameters = {\n", " \"num_class\" : \"6\",\n", " \"num_round\" : \"30\",\n", " \"objective\" : \"multi:softprob\",\n", " \"alpha\" : \"0.08581546561800178\",\n", " \"colsample_bytree\" : \"0.833507617399075\",\n", " \"eta\" : \"0.37501110693093653\",\n", " \"eval_metric\" : \"accuracy,f1,balanced_accuracy,precision_macro,recall_macro\",\n", " \"gamma\" : \"0.016348263861047225\",\n", " \"lambda\" : \"0.059577845107449054\",\n", " \"max_depth\" : \"3\",\n", " \"min_child_weight\" : \"0.000988838943049348\",\n", " \"subsample\" : \"0.5303863656830915\",\n", "}\n", "\n", "output_path = 's3://{}/{}/{}/output'.format(bucket, prefix, 'activity-xgb-framework')" ] }, { "cell_type": "markdown", "id": "643c3077", "metadata": {}, "source": [ "All the XGBoost code is written for us already. So all we need to do is find the SageMaker prebuilt container:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "xgboost_container = sagemaker.image_uris.retrieve(\n", " \"xgboost\", \n", " sagemaker_session.boto_region_name, \n", " \"1.5-1\"\n", ")" ] }, { "cell_type": "markdown", "id": "beeaf87a", "metadata": {}, "source": [ "And then set some specifications for the infrastructure:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "estimator = sagemaker.estimator.Estimator(image_uri=xgboost_container, \n", " hyperparameters=hyperparameters,\n", " role=sagemaker.get_execution_role(),\n", " instance_count=1, \n", " instance_type='ml.m5.2xlarge', \n", " volume_size=5, # 5 GB \n", " output_path=output_path)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Training the Model\n", "\n", "Training the model is now as easy as feeding in our processed training data:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "content_type = \"text/csv\"\n", "train_input = TrainingInput(preprocessed_train, content_type=content_type)" ] }, { "cell_type": "markdown", "id": "ab9fc45b", "metadata": {}, "source": [ "And now running the training job: \n", "\n", "*(Only run this once. If you're trying to reconnect to the notebook, see the next two cells instead.)*" ] }, { "cell_type": "markdown", "id": "a9697fa1", "metadata": {}, "source": [ "![title](img/wait_start.png)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Only run this once. If you're trying to reconnect to the notebook, use the next two cells instead.\n", "\n", "estimator.fit({'train': train_input})" ] }, { "cell_type": "markdown", "id": "c13d3bbc", "metadata": {}, "source": [ "![title](img/wait_end.png)" ] }, { "cell_type": "markdown", "id": "72e5bba0", "metadata": {}, "source": [ "### Need Help? Oh no! I closed the notebook or somehow lost connection and I don't want to train that again....\n", "\n", "Use the following two cells to get back on track. Ask for help if you need. :) " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# echo \"Look for the last run job prefixed sagemaker-xgboost-...\"\n", "# !aws sagemaker list-training-jobs" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# # Paste the name of the last run job from above into this line...\n", "# estimator = estimator.attach('sagemaker-xgboost-xxxxxxxxxxxxxxxxxxxxx', sagemaker_session)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# The Pipeline Model" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "By now we have a trained data transformer (scikit-learn), and a trained ML model (XGBoost). We have everything we need to process data from our phone and make predictions about that new data. \n", "\n", "For each new sample we want to transform, and then have a prediction made. To handle this process we will us an Sagemaker Pipeline Model to perform the orchestration heavy lifting. " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Built models:\n", "\n", "scikit_learn_inferencee_model = sklearn_preprocessor.create_model(\n", " env = {\"SAGEMAKER_DEFAULT_INVOCATIONS_ACCEPT\":\"text/csv\"}\n", ")\n", "\n", "xgb_model = estimator.create_model()\n", "\n", "# Names\n", "\n", "model_name = \"activity-pipeline\"\n", "endpoint_name = \"activity-pipeline\"\n", "\n", "# Create Pipline Model\n", "\n", "sm_model = PipelineModel(\n", " name=model_name, \n", " role=role, \n", " models=[scikit_learn_inferencee_model, xgb_model] # Here is where we define the steps of the pipeline!\n", ")" ] }, { "cell_type": "markdown", "id": "8e35f0ff", "metadata": {}, "source": [ "With our pipeline model now defined. We ask SageMaker to deploy the model to an endpoint. Once complete it will be ready to accept API calls, and make predictions on new data.\n", "\n", "*(Only run this once. If you're trying to reconnect to the notebook, see below.)*" ] }, { "cell_type": "markdown", "id": "a9697fa1", "metadata": {}, "source": [ "![title](img/wait_start.png)" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "# Only run this once. If you're trying to reconnect to the notebook, use the next two cells instead.\n", "\n", "sm_model.deploy(initial_instance_count=1, instance_type=\"ml.c4.xlarge\", endpoint_name=endpoint_name)" ] }, { "cell_type": "markdown", "id": "c13d3bbc", "metadata": {}, "source": [ "![title](img/wait_end.png)" ] }, { "cell_type": "markdown", "id": "238fdea8", "metadata": {}, "source": [ "### Need Help: Oh no! I closed the notebook or somehow lost connection and I don't want to deploy that again....\n", "\n", "That's okay. We don't need to re-run that cell, but we will need to re-run the code up until that point using the commented out helper code and we will be back here again in no time.\n", "\n", "You can monitor the progress of the deploying endpoint here: [https://us-west-2.console.aws.amazon.com/sagemaker/home?region=us-west-2#/endpoints] " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Let's perform a quick test...\n", "\n", "We want to get data streaming from a phone, but just to test everything is working as it should this sample code will perform a single test.\n", "\n", "The output of this cell should be an array of values. These values are prediction scores for each of the labels we have (Run, Walk, etc). The actual predicted action is represented by the highest value in this list. But what are the labels?" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from sagemaker.predictor import Predictor\n", "from sagemaker.serializers import CSVSerializer\n", "\n", "features = \"ax,ay,az,aigx,aigy,aigz,rralpha,rrbeta,rrgamma\"\n", "payload = \"0.44266298739955007,0.7146334002037742,1.3313026220397495,0.467693860580073,0.6073590115775493,1.3592041447373837,16.549713027020594,11.653742763956659,17.214856502392134\"\n", "\n", "predictor = Predictor(\n", " endpoint_name=endpoint_name, sagemaker_session=sagemaker_session, serializer=CSVSerializer()\n", ")\n", "\n", "print(predictor.predict([features, payload]))\n", "\n", "# Should be : walk" ] }, { "cell_type": "markdown", "id": "56e6f275", "metadata": {}, "source": [ "### Let's figure out the labels\n", "\n", "Here is another handy script snippit. It will look for the label csv and process it to produce an ordered list of labels. \n", "\n", "You will need this list, copy and past it into the LABELS environment variable of the Lambda function created by Amplify. (There are more details on this back in the main workshop instructions.)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "for path, subdirs, files in os.walk(\"label_data\"):\n", " for name in files:\n", " if ('.ipynb_checkpoints' not in path) and (name.endswith(\".csv\")): \n", " csv_label_file = os.path.join(path, name)\n", "\n", "df = pd.read_csv(csv_label_file, header=0)\n", "\n", "labels = []\n", "for filename in df.sort_values('label')['_data_source_filename']:\n", " labels.append((filename.split('/')[-1]).split('.')[-2])\n", " \n", "print(','.join(labels))" ] }, { "cell_type": "markdown", "id": "682ffa58", "metadata": {}, "source": [ "*IF* the output is: `recline,walk,desk,run,sit`\n", "\n", "*AND IF* the output from the prediction was: `b'0.00043103244388476014,0.9989290833473206,0.00010582990216789767,0.0001037378387991339,0.0003413469239603728,8.899380190996453e-05\\n'`\n", "\n", "*THEN* the prediction would be: `walk` (In the output the 2nd value is highest, and the 2nd label is 'walk')." ] }, { "cell_type": "markdown", "id": "b438a551", "metadata": {}, "source": [ "## We're done!! That's it! If you deployed the Amplify app it should connect. \n", "\n", "Troubleshooting: Check the environment variables in the Lambda function deployed by Amplify, and ensure the SageMaker endpoint and the labels match the values from this notebook." ] }, { "cell_type": "markdown", "id": "ff38bd3a", "metadata": {}, "source": [ "### EXTRA BONUS\n", "\n", "Once the Amplify app is deployed, use this code to generate a QR code of the site. It's easier than typing it out! (This code will only work if there is just one Amplify app deployed in the account.)\n", "\n", "First we quickly install a QRCode library for Python:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "!pip3 install qrcode" ] }, { "cell_type": "markdown", "id": "3734e04f", "metadata": {}, "source": [ "Import the Python libraries: " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import qrcode\n", "import matplotlib.pyplot as plt" ] }, { "cell_type": "markdown", "id": "543c00ab", "metadata": {}, "source": [ "And generate the QR code: " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "amplify = boto3.client('amplify')\n", "\n", "apps = amplify.list_apps()\n", "appId = apps['apps'][0]['appId']\n", "app = amplify.get_app( appId=appId )\n", "\n", "url = app['app']['defaultDomain']\n", "branchName = app['app']['productionBranch']['branchName']\n", "\n", "image = qrcode.make(\"https://{}.{}\".format(branchName,url))\n", "plt.imshow(image , cmap = 'gray')\n", "plt.show()" ] }, { "cell_type": "markdown", "id": "5ee68757", "metadata": {}, "source": [ "## End of Notebook" ] }, { "cell_type": "markdown", "id": "e784809a", "metadata": {}, "source": [ "```MIT No Attribution\n", "\n", "Copyright 2022 Amazon Web Services\n", "\n", "Permission is hereby granted, free of charge, to any person obtaining a copy of this\n", "software and associated documentation files (the \"Software\"), to deal in the Software\n", "without restriction, including without limitation the rights to use, copy, modify,\n", "merge, publish, distribute, sublicense, and/or sell copies of the Software, and to\n", "permit persons to whom the Software is furnished to do so.\n", "\n", "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,\n", "INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A\n", "PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n", "HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\n", "OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE\n", "SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.```" ] }, { "cell_type": "markdown", "id": "0a3041fa", "metadata": {}, "source": [] } ], "metadata": { "instance_type": "ml.t3.medium", "kernelspec": { "display_name": "Python 3.10.7 64-bit", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.10.7" }, "vscode": { "interpreter": { "hash": "b0fa6594d8f4cbf19f97940f81e996739fb7646882a419484c72d19e05852a7e" } } }, "nbformat": 4, "nbformat_minor": 5 }