diff --git a/program/cohort-old.ipynb b/backup/cohort-7-linear.ipynb similarity index 100% rename from program/cohort-old.ipynb rename to backup/cohort-7-linear.ipynb diff --git a/backup/cohort-7.ipynb b/backup/cohort-7.ipynb new file mode 100644 index 0000000..14860f6 --- /dev/null +++ b/backup/cohort-7.ipynb @@ -0,0 +1,6802 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "00b7ba7b-433c-463c-8e5e-8b975a5be463", + "metadata": { + "tags": [] + }, + "source": [ + "# Building Production Machine Learning Systems\n" + ] + }, + { + "cell_type": "markdown", + "id": "bd7bb73f", + "metadata": {}, + "source": [ + "This notebook creates a [SageMaker Pipeline](https://sagemaker.readthedocs.io/en/stable/amazon_sagemaker_model_building_pipeline.html) to build an end-to-end Machine Learning system to solve the problem of classifying penguin species. With a SageMaker Pipeline, you can create, automate, and manage end-to-end Machine Learning workflows at scale.\n", + "\n", + "You can find more information about Amazon SageMaker in the [Amazon SageMaker Developer Guide](https://docs.aws.amazon.com/sagemaker/latest/dg/whatis.html). The [AWS Machine Learning Blog](https://aws.amazon.com/blogs/machine-learning/) is an excellent source to stay up-to-date with SageMaker.\n", + "\n", + "This example uses the [Penguins dataset](https://www.kaggle.com/parulpandey/palmer-archipelago-antarctica-penguin-data), the [boto3](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/sagemaker.html) library, and the [SageMaker Python SDK](https://sagemaker.readthedocs.io/en/stable/).\n", + "\n", + "Penguins\n", + "\n", + "This notebook is part of the [Machine Learning School](https://www.ml.school) program.\n" + ] + }, + { + "cell_type": "markdown", + "id": "5ec22ac1", + "metadata": {}, + "source": [ + "## Initial setup\n", + "\n", + ":::{.callout-note}\n", + "Before running this notebook, follow the [setup instructions](https://program.ml.school/setup.html) for the program.\n", + ":::\n", + "\n", + "Let's start by setting up the environment and preparing to run the notebook.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 640, + "id": "4b2265b0", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The autoreload extension is already loaded. To reload it, use:\n", + " %reload_ext autoreload\n", + "The dotenv extension is already loaded. To reload it, use:\n", + " %reload_ext dotenv\n" + ] + } + ], + "source": [ + "#| hide\n", + "\n", + "%load_ext autoreload\n", + "%autoreload 2\n", + "%load_ext dotenv\n", + "%dotenv\n", + "\n", + "import sys\n", + "import logging\n", + "import ipytest\n", + "import json\n", + "from pathlib import Path\n", + "\n", + "\n", + "CODE_FOLDER = Path(\"code\")\n", + "CODE_FOLDER.mkdir(parents=True, exist_ok=True)\n", + "INFERENCE_CODE_FOLDER = CODE_FOLDER / \"inference\"\n", + "INFERENCE_CODE_FOLDER.mkdir(parents=True, exist_ok=True)\n", + "\n", + "sys.path.append(f\"./{CODE_FOLDER}\")\n", + "sys.path.append(f\"./{INFERENCE_CODE_FOLDER}\")\n", + "\n", + "DATA_FILEPATH = \"penguins.csv\"\n", + "\n", + "ipytest.autoconfig(raise_on_error=True)\n", + "\n", + "# By default, The SageMaker SDK logs events related to the default\n", + "# configuration using the INFO level. To prevent these from spoiling\n", + "# the output of this notebook cells, we can change the logging\n", + "# level to ERROR instead.\n", + "logging.getLogger(\"sagemaker.config\").setLevel(logging.ERROR)" + ] + }, + { + "cell_type": "markdown", + "id": "588d34c9", + "metadata": {}, + "source": [ + "We can run this notebook is [Local Mode](https://docs.aws.amazon.com/sagemaker/latest/dg/pipelines-local-mode.html) to test the pipeline in your local environment before using SageMaker. You can run the code in Local Mode by setting the `LOCAL_MODE` constant to `True`.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 641, + "id": "32c4d764", + "metadata": {}, + "outputs": [], + "source": [ + "LOCAL_MODE = False" + ] + }, + { + "cell_type": "markdown", + "id": "d6be4f8d", + "metadata": {}, + "source": [ + "Let's load the S3 bucket name and the AWS Role from the environment variables:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 642, + "id": "3164a3af", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "\n", + "bucket = os.environ[\"BUCKET\"]\n", + "role = os.environ[\"ROLE\"]\n", + "\n", + "S3_LOCATION = f\"s3://{bucket}/penguins\"" + ] + }, + { + "cell_type": "markdown", + "id": "daa700f4", + "metadata": {}, + "source": [ + "If you are running the pipeline in Local Mode on an ARM64 machine, you will need to use a custom Docker image to train and evaluate the model. This is because SageMaker doesn't provide a TensorFlow image that supports Apple's M chips.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 643, + "id": "7bc40d28", + "metadata": {}, + "outputs": [], + "source": [ + "architecture = !(uname -m)\n", + "IS_APPLE_M_CHIP = architecture[0] == \"arm64\"" + ] + }, + { + "cell_type": "markdown", + "id": "7d906ada", + "metadata": {}, + "source": [ + "Let's create a configuration dictionary with different settings depending on whether we are running the pipeline in Local Mode or not:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 644, + "id": "3b3f17e5", + "metadata": {}, + "outputs": [], + "source": [ + "import sagemaker\n", + "from sagemaker.workflow.pipeline_context import PipelineSession, LocalPipelineSession\n", + "\n", + "pipeline_session = PipelineSession(default_bucket=bucket) if not LOCAL_MODE else None\n", + "\n", + "if LOCAL_MODE:\n", + " config = {\n", + " \"session\": LocalPipelineSession(default_bucket=bucket),\n", + " \"instance_type\": \"local\",\n", + " # We need to use a custom Docker image when we run the pipeline\n", + " # in Local Model on an ARM64 machine.\n", + " \"image\": \"sagemaker-tensorflow-toolkit-local\" if IS_APPLE_M_CHIP else None,\n", + " \"framework_version\": None if IS_APPLE_M_CHIP else \"2.11\",\n", + " \"py_version\": None if IS_APPLE_M_CHIP else \"py39\",\n", + " }\n", + "else:\n", + " config = {\n", + " \"session\": pipeline_session,\n", + " \"instance_type\": \"ml.m5.xlarge\",\n", + " \"image\": None,\n", + " \"framework_version\": \"2.11\",\n", + " \"py_version\": \"py39\",\n", + " }" + ] + }, + { + "cell_type": "markdown", + "id": "9089696b", + "metadata": {}, + "source": [ + "Let's now initialize a few variables that we'll need throughout the notebook:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 645, + "id": "942a01b5", + "metadata": {}, + "outputs": [], + "source": [ + "import boto3\n", + "\n", + "sagemaker_session = sagemaker.session.Session()\n", + "sagemaker_client = boto3.client(\"sagemaker\")\n", + "iam_client = boto3.client(\"iam\")\n", + "region = boto3.Session().region_name" + ] + }, + { + "cell_type": "markdown", + "id": "11137928-6b4e-465c-8ad7-2297afbaa33c", + "metadata": {}, + "source": [ + "## Session 1 - Production Machine Learning is Different\n", + "\n", + "In this session we'll run Exploratory Data Analysis on the [Penguins dataset](https://www.kaggle.com/parulpandey/palmer-archipelago-antarctica-penguin-data) and we'll build a simple [SageMaker Pipeline](https://docs.aws.amazon.com/sagemaker/latest/dg/pipelines-sdk.html) with one step to split and transform the data. \n", + "\n", + " \"Training\"\n", + "\n", + "We'll use a [Scikit-Learn Pipeline](https://scikit-learn.org/stable/modules/generated/sklearn.pipeline.Pipeline.html) for the transformations, and a [Processing Step](https://docs.aws.amazon.com/sagemaker/latest/dg/build-and-manage-steps.html#step-type-processing) with a [SKLearnProcessor](https://sagemaker.readthedocs.io/en/stable/frameworks/sklearn/sagemaker.sklearn.html#scikit-learn-processor) to execute a preprocessing script. Check the [SageMaker Pipelines Overview](https://docs.aws.amazon.com/sagemaker/latest/dg/pipelines-sdk.html) for an introduction to the fundamental components of a SageMaker Pipeline.\n" + ] + }, + { + "cell_type": "markdown", + "id": "3a835695-557b-46d8-a901-a29bc57df5fe", + "metadata": {}, + "source": [ + "### Step 1 - Exploratory Data Analysis\n", + "\n", + "Let's run Exploratory Data Analysis on the dataset. The goal of this section is to understand the data and the problem we are trying to solve.\n", + "\n", + "Let's load the Penguins dataset:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 646, + "id": "f1cd2f0e-446d-48a9-a008-b4f1cc593bfc", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
speciesislandculmen_length_mmculmen_depth_mmflipper_length_mmbody_mass_gsex
0AdelieTorgersen39.118.7181.03750.0MALE
1AdelieTorgersen39.517.4186.03800.0FEMALE
2AdelieTorgersen40.318.0195.03250.0FEMALE
3AdelieTorgersenNaNNaNNaNNaNNaN
4AdelieTorgersen36.719.3193.03450.0FEMALE
\n", + "
" + ], + "text/plain": [ + " species island culmen_length_mm culmen_depth_mm flipper_length_mm \\\n", + "0 Adelie Torgersen 39.1 18.7 181.0 \n", + "1 Adelie Torgersen 39.5 17.4 186.0 \n", + "2 Adelie Torgersen 40.3 18.0 195.0 \n", + "3 Adelie Torgersen NaN NaN NaN \n", + "4 Adelie Torgersen 36.7 19.3 193.0 \n", + "\n", + " body_mass_g sex \n", + "0 3750.0 MALE \n", + "1 3800.0 FEMALE \n", + "2 3250.0 FEMALE \n", + "3 NaN NaN \n", + "4 3450.0 FEMALE " + ] + }, + "execution_count": 646, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import pandas as pd\n", + "import numpy as np\n", + "\n", + "penguins = pd.read_csv(DATA_FILEPATH)\n", + "penguins.head()" + ] + }, + { + "cell_type": "markdown", + "id": "c9eae10e-20c4-477e-b6b8-965c3a53566e", + "metadata": {}, + "source": [ + "We can see the dataset contains the following columns:\n", + "\n", + "1. `species`: The species of a penguin. This is the column we want to predict.\n", + "2. `island`: The island where the penguin was found\n", + "3. `culmen_length_mm`: The length of the penguin's culmen (bill) in millimeters\n", + "4. `culmen_depth_mm`: The depth of the penguin's culmen in millimeters\n", + "5. `flipper_length_mm`: The length of the penguin's flipper in millimeters\n", + "6. `body_mass_g`: The body mass of the penguin in grams\n", + "7. `sex`: The sex of the penguin\n", + "\n", + "If you are curious, here is the description of a penguin's culmen:\n", + "\n", + "Culmen\n", + "\n", + "Now, let's get the summary statistics for the features in our dataset.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 647, + "id": "f2107c25-e730-4e22-a1b8-5bda53e61124", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
speciesislandculmen_length_mmculmen_depth_mmflipper_length_mmbody_mass_gsex
count344344342.000000342.000000342.000000342.000000334
unique33NaNNaNNaNNaN3
topAdelieBiscoeNaNNaNNaNNaNMALE
freq152168NaNNaNNaNNaN168
meanNaNNaN43.92193017.151170200.9152054201.754386NaN
stdNaNNaN5.4595841.97479314.061714801.954536NaN
minNaNNaN32.10000013.100000172.0000002700.000000NaN
25%NaNNaN39.22500015.600000190.0000003550.000000NaN
50%NaNNaN44.45000017.300000197.0000004050.000000NaN
75%NaNNaN48.50000018.700000213.0000004750.000000NaN
maxNaNNaN59.60000021.500000231.0000006300.000000NaN
\n", + "
" + ], + "text/plain": [ + " species island culmen_length_mm culmen_depth_mm flipper_length_mm \\\n", + "count 344 344 342.000000 342.000000 342.000000 \n", + "unique 3 3 NaN NaN NaN \n", + "top Adelie Biscoe NaN NaN NaN \n", + "freq 152 168 NaN NaN NaN \n", + "mean NaN NaN 43.921930 17.151170 200.915205 \n", + "std NaN NaN 5.459584 1.974793 14.061714 \n", + "min NaN NaN 32.100000 13.100000 172.000000 \n", + "25% NaN NaN 39.225000 15.600000 190.000000 \n", + "50% NaN NaN 44.450000 17.300000 197.000000 \n", + "75% NaN NaN 48.500000 18.700000 213.000000 \n", + "max NaN NaN 59.600000 21.500000 231.000000 \n", + "\n", + " body_mass_g sex \n", + "count 342.000000 334 \n", + "unique NaN 3 \n", + "top NaN MALE \n", + "freq NaN 168 \n", + "mean 4201.754386 NaN \n", + "std 801.954536 NaN \n", + "min 2700.000000 NaN \n", + "25% 3550.000000 NaN \n", + "50% 4050.000000 NaN \n", + "75% 4750.000000 NaN \n", + "max 6300.000000 NaN " + ] + }, + "execution_count": 647, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "penguins.describe(include=\"all\")" + ] + }, + { + "cell_type": "markdown", + "id": "b2e19af7-9f0f-45fe-b7d3-f19721c02a2b", + "metadata": {}, + "source": [ + "Let's now display the distribution of values for the three categorical columns in our data:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 648, + "id": "1242122a-726e-4c37-a718-dd8e873d1612", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "species\n", + "Adelie 152\n", + "Gentoo 124\n", + "Chinstrap 68\n", + "Name: count, dtype: int64\n", + "\n", + "island\n", + "Biscoe 168\n", + "Dream 124\n", + "Torgersen 52\n", + "Name: count, dtype: int64\n", + "\n", + "sex\n", + "MALE 168\n", + "FEMALE 165\n", + ". 1\n", + "Name: count, dtype: int64\n" + ] + } + ], + "source": [ + "species_distribution = penguins[\"species\"].value_counts()\n", + "island_distribution = penguins[\"island\"].value_counts()\n", + "sex_distribution = penguins[\"sex\"].value_counts()\n", + "\n", + "print(species_distribution)\n", + "print()\n", + "print(island_distribution)\n", + "print()\n", + "print(sex_distribution)" + ] + }, + { + "cell_type": "markdown", + "id": "e9d98fdd-3b8c-40a2-b8dc-15162b4049e2", + "metadata": {}, + "source": [ + "The distribution of the categories in our data are:\n", + "\n", + "- `species`: There are 3 species of penguins in the dataset: Adelie (152), Gentoo (124), and Chinstrap (68).\n", + "- `island`: Penguins are from 3 islands: Biscoe (168), Dream (124), and Torgersen (52).\n", + "- `sex`: We have 168 male penguins, 165 female penguins, and 1 penguin with an ambiguous gender ('.').\n", + "\n", + "Let's replace the ambiguous value in the `sex` column with a null value:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 649, + "id": "cf1cf582-8831-4f83-bb17-2175afb193e8", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "sex\n", + "MALE 168\n", + "FEMALE 165\n", + "Name: count, dtype: int64" + ] + }, + "execution_count": 649, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "penguins[\"sex\"] = penguins[\"sex\"].replace(\".\", np.nan)\n", + "penguins[\"sex\"].value_counts()" + ] + }, + { + "cell_type": "markdown", + "id": "6e8425ce-ce4e-43e6-9ed8-0398b780cc66", + "metadata": {}, + "source": [ + "Next, let's check for any missing values in the dataset.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 650, + "id": "cc42cb08-275c-4b05-9d2b-77052da2f336", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "species 0\n", + "island 0\n", + "culmen_length_mm 2\n", + "culmen_depth_mm 2\n", + "flipper_length_mm 2\n", + "body_mass_g 2\n", + "sex 11\n", + "dtype: int64" + ] + }, + "execution_count": 650, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "penguins.isnull().sum()" + ] + }, + { + "cell_type": "markdown", + "id": "1b65207c-3e66-453a-87a1-751636c979ee", + "metadata": {}, + "source": [ + "Let's get rid of the missing values. For now, we are going to replace the missing values with the most frequent value in the column. Later, we'll use a different strategy to replace missing numeric values.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 651, + "id": "3c57d55d-afd6-467a-a7a8-ff04132770ed", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "species 0\n", + "island 0\n", + "culmen_length_mm 0\n", + "culmen_depth_mm 0\n", + "flipper_length_mm 0\n", + "body_mass_g 0\n", + "sex 0\n", + "dtype: int64" + ] + }, + "execution_count": 651, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from sklearn.impute import SimpleImputer\n", + "\n", + "imputer = SimpleImputer(strategy=\"most_frequent\")\n", + "penguins.iloc[:, :] = imputer.fit_transform(penguins)\n", + "penguins.isnull().sum()" + ] + }, + { + "cell_type": "markdown", + "id": "5758214f-a4ab-4980-8892-91ec8d218ef3", + "metadata": {}, + "source": [ + "Let's visualize the distribution of categorical features.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 652, + "id": "2852c740", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAk4AAAPdCAYAAAB83OesAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy81sbWrAAAACXBIWXMAAA9hAAAPYQGoP6dpAACbrUlEQVR4nOzdeVwW5f7/8feNbAoCgsqSCKSmaG6pGWm5UYj7yU7p8Rh6XDrlktopo3LJNNpU1EhPfQvLsr20NM1dy9QU0zbXcjsRUBkgLohw/f7o4fy6A3QklFt8PR+P+/Fwrrnmms/cjPh25rrndhhjjAAAAHBebhVdAAAAwOWC4AQAAGATwQkAAMAmghMAAIBNBCcAAACbCE4AAAA2EZwAAABsIjgBAADYRHACAACwieAEVBKTJ0+Ww+G4JPvq2LGjOnbsaC2vW7dODodD77777iXZ/6BBgxQZGXlJ9lVWeXl5Gjp0qEJCQuRwODRmzJiKLqnMLuW5Bbg6ghPggubPny+Hw2G9vL29FRYWpri4OM2ePVvHjh0rl/2kp6dr8uTJ2rFjR7mMV55cuTY7nnjiCc2fP1/33HOPFixYoIEDB5ba9/Tp05o1a5ZatmwpPz8/BQQEqEmTJho+fLh27959CasGcD4OvqsOcD3z58/X4MGDNWXKFEVFRamgoEAZGRlat26dVq5cqbp16+rDDz9Us2bNrG3OnDmjM2fOyNvb2/Z+tm3bpjZt2ig1NVWDBg2yvd3p06clSZ6enpJ+v+LUqVMnvfPOO7r99tttj1PW2goKClRUVCQvL69y2dfFcMMNN8jd3V2fffbZefv27NlTy5YtU//+/RUTE6OCggLt3r1bS5Ys0eOPP35BP5uLoSznFlBZuVd0AQBKFx8fr9atW1vLiYmJWrNmjXr06KFevXpp165dqlq1qiTJ3d1d7u4X96/0iRMnVK1aNSswVRQPD48K3b8dWVlZaty48Xn7bd26VUuWLNG0adP08MMPO6177rnnlJ2dfZEqtO9SnFvA5YJbdcBlpnPnzpowYYIOHTqk1157zWovaR7KypUr1b59ewUEBMjX11cNGza0/nFet26d2rRpI0kaPHiwdVtw/vz5kn6fx3TttdcqLS1NN998s6pVq2Zt++c5TmcVFhbq4YcfVkhIiHx8fNSrVy8dOXLEqU9kZGSJV1D+OOb5aitpjtPx48d1//33Kzw8XF5eXmrYsKGeffZZ/fmiusPh0MiRI7Vo0SJde+218vLyUpMmTbR8+fKS3/A/ycrK0pAhQxQcHCxvb281b95cr7zyirX+7HyvAwcOaOnSpVbtBw8eLHG877//XpLUrl27YuuqVKmioKAga/nsz3j37t2644475Ofnp6CgIN133306depUse1fe+01tWrVSlWrVlVgYKD69etX7OchSVu2bFG3bt1Uo0YN+fj4qFmzZpo1a1ax/ZZl/H379qlv374KCQmRt7e36tSpo379+iknJ6fE9wNwdQQn4DJ0dr7MihUrSu3z7bffqkePHsrPz9eUKVM0ffp09erVSxs3bpQkRUdHa8qUKZKk4cOHa8GCBVqwYIFuvvlma4xff/1V8fHxatGihZKTk9WpU6dz1jVt2jQtXbpU48eP1+jRo7Vy5UrFxsbq5MmTF3R8dmr7I2OMevXqpZkzZ6pr166aMWOGGjZsqAceeEDjxo0r1v+zzz7Tvffeq379+unpp5/WqVOn1LdvX/3666/nrOvkyZPq2LGjFixYoAEDBuiZZ56Rv7+/Bg0aZAWN6OhoLViwQDVr1lSLFi2s2mvVqlXimBEREZKk119/XWfOnLH1/txxxx06deqUkpKS1K1bN82ePVvDhw936jNt2jTdddddatCggWbMmKExY8Zo9erVuvnmm52uYq1cuVI333yzvvvuO913332aPn26OnXqpCVLlpyzBjvjnz59WnFxcdq8ebNGjRqllJQUDR8+XD/88INLXEkDysQAcDmpqalGktm6dWupffz9/U3Lli2t5UmTJpk//pWeOXOmkWR+/vnnUsfYunWrkWRSU1OLrevQoYORZObNm1fiug4dOljLa9euNZLMVVddZXJzc632t99+20gys2bNstoiIiJMQkLCecc8V20JCQkmIiLCWl60aJGRZKZOnerU7/bbbzcOh8Ps37/fapNkPD09ndp27txpJJk5c+YU29cfJScnG0nmtddes9pOnz5tYmJijK+vr9OxR0REmO7du59zPGOMKSoqst7r4OBg079/f5OSkmIOHTpUrO/Zn3GvXr2c2u+9914jyezcudMYY8zBgwdNlSpVzLRp05z6ff3118bd3d1qP3PmjImKijIRERHmt99+K1bXn/d7lt3xv/zySyPJvPPOO+d9H4DLBVecgMuUr6/vOT9dFxAQIElavHixioqKyrQPLy8vDR482Hb/u+66S9WrV7eWb7/9doWGhurjjz8u0/7t+vjjj1WlShWNHj3aqf3++++XMUbLli1zao+NjVW9evWs5WbNmsnPz08//PDDefcTEhKi/v37W20eHh4aPXq08vLytH79+guu3eFw6JNPPtHUqVNVo0YNvfHGGxoxYoQiIiJ05513lnhlZsSIEU7Lo0aNsuqTpPfff19FRUW644479Msvv1ivkJAQNWjQQGvXrpUkffnllzpw4IDGjBljnS9/rKs0dsf39/eXJH3yySc6ceLEBb83gCsiOAGXqby8PKeQ8md33nmn2rVrp6FDhyo4OFj9+vXT22+/fUEh6qqrrrqgieANGjRwWnY4HKpfv36p83vKy6FDhxQWFlbs/YiOjrbW/1HdunWLjVGjRg399ttv591PgwYN5Obm/KuztP3Y5eXlpUceeUS7du1Senq63njjDd1www16++23NXLkyGL9//w+16tXT25ubtb7vG/fPhlj1KBBA9WqVcvptWvXLmVlZUn6//Orrr322guq1+74UVFRGjdunP7v//5PNWvWVFxcnFJSUpjfhMsaH5MALkP/+9//lJOTo/r165fap2rVqtqwYYPWrl2rpUuXavny5XrrrbfUuXNnrVixQlWqVDnvfs5+Yq88lXYlo7Cw0FZN5aG0/RgXeDpLaGio+vXrp759+6pJkyZ6++23NX/+/HN+qu3P72lRUZEcDoeWLVtW4rH6+vr+pRovZPzp06dr0KBBWrx4sVasWKHRo0crKSlJmzdvVp06df5SHUBFIDgBl6EFCxZIkuLi4s7Zz83NTV26dFGXLl00Y8YMPfHEE3rkkUe0du1axcbGlvvToPft2+e0bIzR/v37nZ43VaNGjRJvPx06dEhXX321tXwhtUVERGjVqlU6duyY01Wnsw+PPDsB+6+KiIjQV199paKiIqerTuW9H+n3W4DNmjXTvn37rNtgZ+3bt09RUVHW8v79+1VUVGR90rBevXoyxigqKkrXXHNNqfs4e7vym2++UWxsrO3a7I5/VtOmTdW0aVM9+uij+vzzz9WuXTvNmzdPU6dOtb1PwFVwqw64zKxZs0aPP/64oqKiNGDAgFL7HT16tFhbixYtJEn5+fmSJB8fH0kqt084vfrqq07zrt5991399NNPio+Pt9rq1aunzZs3Ww/RlKQlS5YU+xj7hdTWrVs3FRYW6rnnnnNqnzlzphwOh9P+/4pu3bopIyNDb731ltV25swZzZkzR76+vurQocMFj7lv3z4dPny4WHt2drY2bdqkGjVqFPtEXkpKitPynDlzJMk6zttuu01VqlTRY489VuwqmjHG+vTgddddp6ioKCUnJxd7n8919c3u+Lm5ucU+Kdi0aVO5ublZ5yBwueGKE+DCli1bpt27d+vMmTPKzMzUmjVrtHLlSkVEROjDDz8855Ocp0yZog0bNqh79+6KiIhQVlaWnn/+edWpU0ft27eX9HuICQgI0Lx581S9enX5+Piobdu2TlczLkRgYKDat2+vwYMHKzMzU8nJyapfv76GDRtm9Rk6dKjeffddde3aVXfccYe+//57vfbaa06TtS+0tp49e6pTp0565JFHdPDgQTVv3lwrVqzQ4sWLNWbMmGJjl9Xw4cP13//+V4MGDVJaWpoiIyP17rvvauPGjUpOTj7nnLPS7Ny5U//4xz8UHx+vm266SYGBgfrxxx/1yiuvKD09XcnJycVuhx04cEC9evVS165dtWnTJr322mv6xz/+oebNm0v6/b2bOnWqEhMTdfDgQfXp00fVq1fXgQMH9MEHH2j48OH6z3/+Izc3N82dO1c9e/ZUixYtNHjwYIWGhmr37t369ttv9cknn5RYs93x16xZo5EjR+rvf/+7rrnmGp05c0YLFixQlSpV1Ldv3wv/AQCuoGI+zAfgXM4+juDsy9PT04SEhJhbbrnFzJo1y+lj72f9+SPjq1evNr179zZhYWHG09PThIWFmf79+5u9e/c6bbd48WLTuHFj4+7u7vTx/w4dOpgmTZqUWF9pjyN44403TGJioqldu7apWrWq6d69e4kfq58+fbq56qqrjJeXl2nXrp3Ztm1bsTHPVdufH0dgjDHHjh0zY8eONWFhYcbDw8M0aNDAPPPMM04fqzfm98cRjBgxolhNpT0m4c8yMzPN4MGDTc2aNY2np6dp2rRpiY9MsPs4gszMTPPkk0+aDh06mNDQUOPu7m5q1KhhOnfubN59912nvmd/xt999525/fbbTfXq1U2NGjXMyJEjzcmTJ4uN/d5775n27dsbHx8f4+PjYxo1amRGjBhh9uzZ49Tvs88+M7fccoupXr268fHxMc2aNXN6NMOfzy274//www/mX//6l6lX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gE0EJwAAAJsITgAAADYRnAAAAGwiOAEAANhEcAIAALCpTMHp6quv1q+//lqsPTs7W1dfffVfLgoAAMAVlSk4HTx4UIWFhcXa8/Pz9eOPP/7logAAAFzRBX3J74cffmj9+ZNPPpG/v7+1XFhYqNWrVysyMrLcigMAAHAlFxSc+vTpI0lyOBxKSEhwWufh4aHIyEhNnz693IoDAABwJRcUnIqKiiRJUVFR2rp1q2rWrHlRigIAAHBFFxSczjpw4EB51wEAAODyyhScJGn16tVavXq1srKyrCtRZ7388st/uTAAAABXU6bg9Nhjj2nKlClq3bq1QkND5XA4yrsuAAAAl1Om4DRv3jzNnz9fAwcOLO96AAAAXFaZnuN0+vRp3XjjjeVdCwAAgEsrU3AaOnSoFi5cWN61AAAAuLQy3ao7deqUXnjhBa1atUrNmjWTh4eH0/oZM2aUS3EAAACupEzB6auvvlKLFi0kSd98843TOiaKAwCAyqpMwWnt2rXlXUeJCgsLNXnyZL322mvKyMhQWFiYBg0apEcffdQKaMYYTZo0SS+++KKys7PVrl07zZ07Vw0aNLgkNQIAgCtHmeY4XSpPPfWU5s6dq+eee067du3SU089paefflpz5syx+jz99NOaPXu25s2bpy1btsjHx0dxcXE6depUBVYOAAAqozJdcerUqdM5b8mtWbOmzAX90eeff67evXure/fukqTIyEi98cYb+uKLLyT9frUpOTlZjz76qHr37i1JevXVVxUcHKxFixapX79+5VIHAACAVMYrTi1atFDz5s2tV+PGjXX69Glt375dTZs2LbfibrzxRq1evVp79+6VJO3cuVOfffaZ4uPjJf3+1S8ZGRmKjY21tvH391fbtm21adOmUsfNz89Xbm6u0wsAAOB8ynTFaebMmSW2T548WXl5eX+poD966KGHlJubq0aNGqlKlSoqLCzUtGnTNGDAAElSRkaGJCk4ONhpu+DgYGtdSZKSkvTYY4+VW50AAODKUK5znP75z3+W6/fUvf3223r99de1cOFCbd++Xa+88oqeffZZvfLKK39p3MTEROXk5FivI0eOlFPFAACgMivzl/yWZNOmTfL29i638R544AE99NBD1lylpk2b6tChQ0pKSlJCQoJCQkIkSZmZmQoNDbW2y8zMtB6XUBIvLy95eXmVW50AAODKUKbgdNtttzktG2P0008/adu2bZowYUK5FCZJJ06ckJub80WxKlWqqKioSJIUFRWlkJAQrV692gpKubm52rJli+65555yqwMAAEAqY3Dy9/d3WnZzc1PDhg01ZcoU3XrrreVSmCT17NlT06ZNU926ddWkSRN9+eWXmjFjhv71r39J+v1hm2PGjNHUqVPVoEEDRUVFacKECQoLC1OfPn3KrQ4AAACpjMEpNTW1vOso0Zw5czRhwgTde++9ysrKUlhYmO6++25NnDjR6vPggw/q+PHjGj58uLKzs9W+fXstX768XG8ZAgAASJLDGGPKunFaWpp27dolSWrSpIlatmxZboVdSrm5ufL391dOTo78/PzKffzIh5aW+5i4/B18sntFl8C5iRK5wrkJXEoXkgPKdMUpKytL/fr107p16xQQECBJys7OVqdOnfTmm2+qVq1aZRkWAADApZXpcQSjRo3SsWPH9O233+ro0aM6evSovvnmG+Xm5mr06NHlXSMAAIBLKNMVp+XLl2vVqlWKjo622ho3bqyUlJRynRwOAADgSsp0xamoqEgeHh7F2j08PKxHBQAAAFQ2ZQpOnTt31n333af09HSr7ccff9TYsWPVpUuXcisOAADAlZQpOD333HPKzc1VZGSk6tWrp3r16ikqKkq5ubmaM2dOedcIAADgEso0xyk8PFzbt2/XqlWrtHv3bklSdHS0YmNjy7U4AAAAV3JBV5zWrFmjxo0bKzc3Vw6HQ7fccotGjRqlUaNGqU2bNmrSpIk+/fTTi1UrAABAhbqg4JScnKxhw4aV+HAof39/3X333ZoxY0a5FQcAAOBKLig47dy5U127di11/a233qq0tLS/XBQAAIAruqDglJmZWeJjCM5yd3fXzz///JeLAgAAcEUXFJyuuuoqffPNN6Wu/+qrrxQaGvqXiwIAAHBFFxScunXrpgkTJujUqVPF1p08eVKTJk1Sjx49yq04AAAAV3JBjyN49NFH9f777+uaa67RyJEj1bBhQ0nS7t27lZKSosLCQj3yyCMXpVAAwJUj8qGlFV0CXNDBJ7tXdAkXFpyCg4P1+eef65577lFiYqKMMZIkh8OhuLg4paSkKDg4+KIUCgAAUNEu+AGYERER+vjjj/Xbb79p//79MsaoQYMGqlGjxsWoDwAAwGWU6cnhklSjRg21adOmPGsBAABwaWX6rjoA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AACbCE4AAAA2EZxQ7iZPnqwWLVrY7n/w4EE5HA7t2LFDkrRu3To5HA5lZ2dflPoAoLw5HA4tWrSo1PX8Xqs8CE6wZdOmTapSpYq6d+9+0fd144036qeffrL9MDJcOTIyMnTfffepfv368vb2VnBwsNq1a6e5c+fqxIny+26zjh07asyYMeU2Hi5/GRkZGjVqlK6++mp5eXkpPDxcPXv2dHqG4LmU9++1C/0PKspPpXgcAS6+l156SaNGjdJLL72k9PR0W4+lLytPT89Sn/qOK9cPP/ygdu3aKSAgQE888YSaNm0qLy8vff3113rhhRd01VVXqVevXhVdJiqhgwcPWufeM888o6ZNm6qgoECffPKJRowYUer3ov5RRf1eKygokIeHxyXfb6VmgPM4duyY8fX1Nbt37zZ33nmnmTZtmtP6pKQkU7t2bePr62v+9a9/mfHjx5vmzZs79XnxxRdNo0aNjJeXl2nYsKFJSUmx1h04cMBIMl9++aUxxpi1a9caSea3336z+nz66aemffv2xtvb29SpU8eMGjXK5OXlXaxDhguKi4szderUKfXnXlRUZIwx5rfffjNDhgwxNWvWNNWrVzedOnUyO3bssPpNmjTJNG/e3Lz66qsmIiLC+Pn5mTvvvNPk5uYaY4xJSEgwkpxeBw4cMMYYs27dOtOmTRvj6elpQkJCzPjx401BQYE19qlTp8yoUaNMrVq1jJeXl2nXrp354osvLtI7gkslPj7eXHXVVSWee2d/T0kyL774ounTp4+pWrWqqV+/vlm8eLHV78+/11JTU42/v79Zvny5adSokfHx8TFxcXEmPT3daZs2bdqYatWqGX9/f3PjjTeagwcPmtTU1GLnaGpqqlXH888/b3r27GmqVatmJk2aZM6cOWP+9a9/mcjISOPt7W2uueYak5yc7HQcCQkJpnfv3mby5MnW3527777b5Ofnl++bWQkQnHBeL730kmndurUxxpiPPvrI1KtXz/pH6q233jJeXl7m//7v/8zu3bvNI488YqpXr+4UnF577TUTGhpq3nvvPfPDDz+Y9957zwQGBpr58+cbY84fnPbv3298fHzMzJkzzd69e83GjRtNy5YtzaBBgy7Ze4CK9csvvxiHw2GSkpLO2zc2Ntb07NnTbN261ezdu9fcf//9JigoyPz666/GmN+Dk6+vr7ntttvM119/bTZs2GBCQkLMww8/bIwxJjs728TExJhhw4aZn376yfz000/mzJkz5n//+5+pVq2auffee82uXbvMBx98YGrWrGkmTZpk7Xv06NEmLCzMfPzxx+bbb781CQkJpkaNGta+cfn59ddfjcPhME888cQ5+0kyderUMQsXLjT79u0zo0ePNr6+vtbPvqTg5OHhYWJjY83WrVtNWlqaiY6ONv/4xz+MMcYUFBQYf39/85///Mfs37/ffPfdd2b+/Pnm0KFD5sSJE+b+++83TZo0sc7REydOWHXUrl3bvPzyy+b77783hw4dMqdPnzYTJ040W7duNT/88IN57bXXTLVq1cxbb71l1Z+QkGB8fX3NnXfeab755huzZMkSU6tWLevvBf4/ghPO68Ybb7T+d1JQUGBq1qxp1q5da4wxJiYmxtx7771O/du2besUnOrVq2cWLlzo1Ofxxx83MTExxpjzB6chQ4aY4cOHO23/6aefGjc3N3Py5MlyOkq4ss2bNxtJ5v3333dqDwoKMj4+PsbHx8c8+OCD5tNPPzV+fn7m1KlTTv3q1atn/vvf/xpjfg9O1apVs64wGWPMAw88YNq2bWstd+jQwdx3331OYzz88MOmYcOG1n8ajDEmJSXF+Pr6msLCQpOXl2c8PDzM66+/bq0/ffq0CQsLM08//fRffg9QMbZs2VLiufdnksyjjz5qLefl5RlJZtmyZcaYkoOTJLN//35rm5SUFBMcHGyM+T2wSTLr1q0rcX9nr5yWVMeYMWPOe1wjRowwffv2tZYTEhJMYGCgOX78uNU2d+5c6/zG/8fkcJzTnj179MUXX6h///6SJHd3d91555166aWXJEm7du1S27Ztnbb545crHz9+XN9//72GDBkiX19f6zV16lR9//33tmrYuXOn5s+f77R9XFycioqKdODAgXI6UlyOvvjiC+3YsUNNmjRRfn6+du7cqby8PAUFBTmdLwcOHHA63yIjI1W9enVrOTQ0VFlZWefc165duxQTEyOHw2G1tWvXTnl5efrf//6n77//XgUFBWrXrp213sPDQ9dff7127dpVjkeNS8lcwJdrNGvWzPqzj4+P/Pz8znleVatWTfXq1bOW/3geBgYGatCgQYqLi1PPnj01a9Ys/fTTT7bqaN26dbG2lJQUtWrVSrVq1ZKvr69eeOEFHT582KlP8+bNVa1aNWs5JiZGeXl5OnLkiK39XimYHI5zeumll3TmzBmnyeDGGHl5eem555477/Z5eXmSpBdffLFYwKpSpYqtGvLy8nT33Xdr9OjRxdbVrVvX1hi4vNWvX18Oh0N79uxxar/66qslSVWrVpX0+7kSGhqqdevWFRsjICDA+vOfJ8s6HA4VFRWVb9GoFBo0aCCHw2FrAviFnlcl9f9jUEtNTdXo0aO1fPlyvfXWW3r00Ue1cuVK3XDDDeesw8fHx2n5zTff1H/+8x9Nnz5dMTExql69up555hlt2bLlvMeE4ghOKNWZM2f06quvavr06br11lud1vXp00dvvPGGoqOjtWXLFt11113Wus2bN1t/Dg4OVlhYmH744QcNGDCgTHVcd911+u6771S/fv2yHQgue0FBQbrlllv03HPPadSoUcX+YTjruuuuU0ZGhtzd3RUZGVnm/Xl6eqqwsNCpLTo6Wu+9956MMdZVp40bN6p69eqqU6eOgoKC5OnpqY0bNyoiIkLS759o2rp1K482uIwFBgYqLi5OKSkpGj16dLFzLzs72ymUl7eWLVuqZcuWSkxMVExMjBYuXKgbbrihxHO0NBs3btSNN96oe++912or6Yr/zp07dfLkSes/Ips3b5avr6/Cw8PL52AqCW7VoVRLlizRb7/9piFDhujaa691evXt21cvvfSS7rvvPr388stKTU3V3r17NWnSJH377bdO4zz22GNKSkrS7NmztXfvXn399ddKTU3VjBkzbNUxfvx4ff755xo5cqR27Nihffv2afHixRo5cuTFOGy4qOeff15nzpxR69at9dZbb2nXrl3as2ePXnvtNe3evVtVqlRRbGysYmJi1KdPH61YsUIHDx7U559/rkceeUTbtm2zva/IyEht2bJFBw8e1C+//KKioiLde++9OnLkiEaNGqXdu3dr8eLFmjRpksaNGyc3Nzf5+Pjonnvu0QMPPKDly5fru+++07Bhw3TixAkNGTLkIr4zuNhSUlJUWFio66+/Xu+995727dunXbt2afbs2U5TE8rTgQMHlJiYqE2bNunQoUNasWKF9u3bp+joaEm/n6MHDhzQjh079Msvvyg/P7/UsRo0aKBt27bpk08+0d69ezVhwgRt3bq1WL/Tp09ryJAh+u677/Txxx9r0qRJGjlypNzciApOKnaKFVxZjx49TLdu3Upcd3bC5M6d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bbWdOXPG3HTTTaX+bIDLHbfqgErK19f3nJ+uCwgIkCQtXry4zBOpvby8NHjwYNv977rrLlWvXt1avv322xUaGqqPP/64TPu36+OPP1aVKlU0evRop/b7779fxhgtW7bMqT02Nlb16tWzlps1ayY/Pz/98MMP591PSEiI+vfvb7V5eHho9OjRysvL0/r168vhaH7/2R0/flwrV668oO3+eFXv+PHj+uWXX3TjjTfKGKMvv/yyWP9///vfTss33XST03vw8ccfy93d3boCJUlVqlTRqFGjLqgu4HJCcAIqqby8PKeQ8md33nmn2rVrp6FDhyo4OFj9+vXT22+/fUEh6qqrrrqgieANGjRwWnY4HKpfv/4Fz++5UIcOHVJYWFix9yM6Otpa/0d169YtNkaNGjX022+/nXc/DRo0kJub86/W0vZTVvfee6+uueYaxcfHq06dOvrXv/5law7W4cOHNWjQIAUGBlrzljp06CBJysnJcerr7e1d7Pbhn9+DQ4cOKTQ0VL6+vk79GjZsWNZDA1wewQmohP73v/8pJydH9evXL7VP1apVtWHDBq1atUoDBw7UV199pTvvvFO33HJLsQnA5xqjvJX2kE67NZWHKlWqlNhu/jSRvKLUrl1bO3bs0IcffmjN3YqPjz/n3KLCwkLdcsstWrp0qcaPH69FixZp5cqV1oT6Pwfm0t4D4EpHcAIqoQULFkiS4uLiztnPzc1NXbp00YwZM/Tdd99p2rRpWrNmjdauXSup9BBTVvv27XNaNsZo//79Tp+Aq1GjRomfyvrz1ZoLqS0iIkLp6enFbl3u3r3bWl8eIiIitG/fvmIhpLz3I0menp7q2bOnnn/+eX3//fe6++679eqrr2r//v0l9v/666+1d+9eTZ8+XePHj1fv3r0VGxursLCwMtcQERGhn376SXl5eU7te/bsKfOYgKsjOAGVzJo1a/T4448rKipKAwYMKLXf0aNHi7WdfZBkfn6+JMnHx0eSyu3j5a+++qpTeHn33Xf1008/KT4+3mqrV6+eNm/erNOnT1ttS5YsKfbYgguprVu3biosLNRzzz3n1D5z5kw5HA6n/f8V3bp1U0ZGht566y2r7cyZM5ozZ458fX2t22J/1a+//uq07ObmZj3s9OzP7s/OXkH641UzY8x5H2FwLt26ddOZM2c0d+5cq62wsFBz5swp85iAq+NxBMBlbNmyZdq9e7fOnDmjzMxMrVmzRitXrlRERIQ+/PBDeXt7l7rtlClTtGHDBnXv3l0RERHKysrS888/rzp16qh9+/aSfg8xAQEBmjdvnqpXry4fHx+1bdtWUVFRZao3MDBQ7du31+DBg5WZmank5GTVr1/f6ZEJQ4cO1bvvvquuXbvqjjvu0Pfff6/XXnvNabL2hdbWs2dPderUSY888ogOHjyo5s2ba8WKFVq8eLHGjBlTbOyyGj58uP773/9q0KBBSktLU2RkpN59911t3LhRycnJ55xzdiGGDh2qo0ePqnPnzqpTp44OHTqkOXPmqEWLFtZ8qj9r1KiR6tWrp//85z/68ccf5efnp/fee++887bOpWfPnmrXrp0eeughHTx4UI0bN9b7779fbL4UUKlU5Ef6AJTN2ccRnH15enqakJAQc8stt5hZs2Y5fez9rD8/jmD16tWmd+/eJiwszHh6epqwsDDTv39/s3fvXqftFi9ebBo3bmzc3d2dPmLeoUMH06RJkxLrK+1xBG+88YZJTEw0tWvXNlWrVjXdu3d3+hj8WdOnTzdXXXWV8fLyMu3atTPbtm0rNua5avvz4wiMMebYsWNm7NixJiwszHh4eJgGDRqYZ555xhQVFTn1k2RGjBhRrKbSHpPwZ5mZmWbw4MGmZs2axtPT0zRt2rTEj+X/lccRvPvuu+bWW281tWvXNp6enqZu3brm7rvvNj/99JPVp6THEXz33XcmNjbW+Pr6mpo1a5phw4ZZj1r4Y40JCQnGx8enWB1/PoeMMebXX381AwcONH5+fsbf398MHDjQfPnllzyOAJWWwxgXme0IAADg4pjjBAAAYBPBCQAAwCaCEwAAgE0EJwAAAJsITgAAADYRnAAAAGziAZj6/Tua0tPTVb169XL/igkAAODajDE6duyYwsLCin1J958RnCSlp6crPDy8ossAAAAV6MiRI6pTp845+xCcJOtrEI4cOSI/P78KrgYAAFxKubm5Cg8Pt/W1SAQn/f9vWffz8yM4AQBwhbIzXYfJ4QAAADYRnAAAAGwiOAEAANhEcAIAALCJ4AQAAGATwQkAAMAmghMAAIBNBCcAAACbCE4AAAA28eTwSyDyoaUVXQJc0MEnu1d0CQCAC8QVJwAAAJsITgAAADYRnAAAAGwiOAEAANhEcAIAALCJ4AQAAGATwQkAAMAmghMAAIBNFRqcNmzYoJ49eyosLEwOh0OLFi0q1mfXrl3q1auX/P395ePjozZt2ujw4cPW+lOnTmnEiBEKCgqSr6+v+vbtq8zMzEt4FAAA4EpRocHp+PHjat68uVJSUkpc//3336t9+/Zq1KiR1q1bp6+++koTJkyQt7e31Wfs2LH66KOP9M4772j9+vVKT0/XbbfddqkOAQAAXEEq9CtX4uPjFR8fX+r6Rx55RN26ddPTTz9ttdWrV8/6c05Ojl566SUtXLhQnTt3liSlpqYqOjpamzdv1g033HDxigcAAFccl53jVFRUpKVLl+qaa65RXFycateurbZt2zrdzktLS1NBQYFiY2OttkaNGqlu3bratGlTqWPn5+crNzfX6QUAAHA+LhucsrKylJeXpyeffFJdu3bVihUr9Le//U233Xab1q9fL0nKyMiQp6enAgICnLYNDg5WRkZGqWMnJSXJ39/feoWHh1/MQwEAAJWEywanoqIiSVLv3r01duxYtWjRQg899JB69OihefPm/aWxExMTlZOTY72OHDlSHiUDAIBKrkLnOJ1LzZo15e7ursaNGzu1R0dH67PPPpMkhYSE6PTp08rOzna66pSZmamQkJBSx/by8pKXl9dFqRsAAFReLnvFydPTU23atNGePXuc2vfu3auIiAhJUqtWreTh4aHVq1db6/fs2aPDhw8rJibmktYLAAAqvwq94pSXl6f9+/dbywcOHNCOHTsUGBiounXr6oEHHtCdd96pm2++WZ06ddLy5cv10Ucfad26dZIkf39/DRkyROPGjVNgYKD8/Pw0atQoxcTE8Ik6AABQ7io0OG3btk2dOnWylseNGydJSkhI0Pz58/W3v/1N8+bNU1JSkkaPHq2GDRvqvffeU/v27a1tZs6cKTc3N/Xt21f5+fmKi4vT888/f8mPBQAAVH4OY4yp6CIqWm5urvz9/ZWTkyM/P79yHz/yoaXlPiYufwef7F7RJQAAdGE5wGXnOAEAALgaghMAAIBNBCcAAACbCE4AAAA2EZwAAABsIjgBAADYRHACAACwieAEAABgE8EJAADAJoITAACATQQnAAAAmwhOAAAANhGcAAAAbCI4AQAA2ERwAgAAsIngBAAAYBPBCQAAwCaCEwAAgE0EJwAAAJsqNDht2LBBPXv2VFhYmBwOhxYtWlRq33//+99yOBxK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AQAAuJoyBafU1NTyrgMAAMDllSk4nZWWlqZdu3ZJkpo0aaKWLVuWS1EAAACuqEzBKSsrS/369dO6desUEBAgScrOzlanTp305ptvqlatWuVZIwAAgEso06fqRo0apWPHjunbb7/V0aNHdfToUX3zzTfKzc3V6NGjy7tGAAAAl1Cm4LR8+XI9//zzio6OttoaN26slJQULVu2rNyKKyws1IQJExQVFaWqVauqXr16evzxx2WMsfoYYzRx4kSFhoaqatWqio2N1b59+8qtBgAAgLPKFJyKiork4eFRrN3Dw0NFRUV/uaiznnrqKc2dO1fPPfecdu3apaeeekpPP/205syZY/V5+umnNXv2bM2bN09btmyRj4+P4uLidOrUqXKrAwAAQCpjcOrcubPuu+8+paenW20//vijxo4dqy5dupRbcZ9//rl69+6t7t27KzIyUrfffrtuvfVWffHFF5J+v9qUnJysRx99VL1791azZs306quvKj09XYsWLSq3OgAAAKQyBqfnnntOubm5ioyMVL169VSvXj1FRUUpNzfX6WrQX3XjjTdq9erV2rt3ryRp586d+uyzzxQfHy9JOnDggDIyMhQbG2tt4+/vr7Zt22rTpk2ljpufn6/c3FynFwAAwPmU6VN14eHh2r59u1atWqXdu3dLkqKjo50CTHl46KGHlJubq0aNGqlKlSoqLCzUtGnTNGDAAElSRkaGJCk4ONhpu+DgYGtdSZKSkvTYY4+Va60AAKDyu6ArTmvWrFHjxo2Vm5srh8OhW265RaNGjdKoUaPUpk0bNWnSRJ9++mm5Fff222/r9ddf18KFC7V9+3a98sorevbZZ/XKK6/8pXETExOVk5NjvY4cOVJOFQMAgMrsgq44JScna9iwYfLz8yu2zt/fX3fffbdmzJihm266qVyKe+CBB/TQQw+pX79+kqSmTZvq0KFDSkpKUkJCgkJCQiRJmZmZCg0NtbbLzMxUixYtSh3Xy8tLXl5e5VIjAAC4clzQFaedO3eqa9eupa6/9dZblZaW9peLOuvEiRNyc3MusUqVKtYn96KiohQSEqLVq1db63Nzc7VlyxbFxMSUWx0AAADSBV5xyszMLPExBNZg7u76+eef/3JRZ/Xs2VPTpk1T3bp11aRJE3355ZeaMWOG/vWvf0mSHA6HxowZo6lTp6pBgwaKiorShAkTFBYW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erQCAgIUFBSk8ePHKyEhQX369LG2LSoqUlJSkqKiolS1alU1b95c7777rrV+3bp1cjgcWrZsmVq1aiUvLy999tln2rlzpzp16qTq1avLz89PrVq10rZt26ztPvvsM910002qWrWqwsPDNXr0aB0/ftxaHxkZqSeeeEL/+te/VL16ddWtW1cvvPDCxX2TL0cGKEVCQoLp3bu3tXzs2DFz9913m/r165vCwkJz4MABI8l8+eWXxhhjRowYYVq0aGG2bt1qDhw4YFauXGk+/PBDa9urr77a3HTTTebTTz81+/btM2+99Zb5/PPPjTHGvP/++8bDw8OkpKSYPXv2mOnTp5sqVaqYNWvWWPuPjY01PXv2NFu3bjV79+41999/vwkKCjK//vrrJXtPUPH+fF7+UfPmzU18fLwxxhhJpnbt2ubll18233//vTl06JDZsGGD8fPzM/Pnzzfff/+9WbFihYmMjDSTJ0+2xpg5c6ZZs2aNOXDggFm9erVp2LChueeee6z1qampxsPDw9xyyy1m+/btZv369SYoKMjceuut5o477jDffvut+eijj4ynp6d58803L+p7gUsnNTXV+Pv7W8szZswwfn5+5o033jC7d+82Dz74oPHw8DB79+41xhjr92NkZKR57733zA8//GDS09PN1KlTTWBgoHn//ffNrl27zL///W/j5+fndE5PnTrVNGrUyCxfvtx8//33JjU11Xh5eZl169YZY4xZu3atkWSaNWtmVqxYYfbv329+/fVX06RJE/PPf/7T7Nq1y+zdu9e8/fbbZseOHcYYY/bv3298fHzMzJkzzd69e83GjRtNy5YtzaBBg6z9RkREmMDAQJOSkmL27dtnkpKSjJubm9m9e/fFf4MvIwQnlCohIcFUqVLF+Pj4GB8fHyPJhIaGmrS0NGOMKRacevbsaQYPHlziWP/9739N9erVSw05N954oxk2bJhT29///nfTrVs3Y4wxn376qfHz8zOnTp1y6lOvXj3z3//+968cJi4z5wpOd955p4mOjjbG/B6cxowZ47S+S5cu5oknnnBqW7BggQkNDS11f++8844JCgqyllNTU40ks3//fqvt7rvvNtWqVTPHjh2z2uLi4szdd99t+7jg2v4cnMLCwsy0adOc+rRp08bce++9xpj///sxOTnZqU9wcLB55plnrOUzZ86YunXrWuf0qVOnTLVq1az/VJ41ZMgQ079/f2PM/w9OixYtcupTvXp1M3/+/BLrHzJkiBk+fLhT26effmrc3NzMyZMnjTG/B6d//vOf1vqioiJTu3ZtM3fu3BLHvFIxOQXn1KlTJ82dO1fS77finn/+ecXHx+uLL74o1veee+5R3759tX37dt16663q06ePbrzxRknSjh071LJlSwUGBpa4n127dmn48OFObe3atdOsWbMkSTt37lReXp6CgoKc+pw8eVLff//9Xz5OVA7GGDkcDmu5devWTut37typjRs3WrftJKmwsFCnTp3SiRMnVK1aNa1atUpJSUnavXu3cnNzdebMGaf1klStWjXVq1fPGiM4OFiRkZHy9fV1asvKyrpYh4oKlJubq/T0dLVr186pvV27dsWmD/zxHMzJyVFmZqauv/56q61KlSpq1aqV9cGG/fv368SJE7rlllucxjl9+rRatmxZ6tjS7194P3ToUC1YsECxsbH6+9//bp2nO3fu1FdffaXXX3/d6m+MUVFRkQ4cOKDo6GhJUrNmzaz1Z29Nch47IzjhnHx8fFS/fn1r+f/+7//k7++vF198UUOHDnXqGx8fr0OHDunjjz/WypUr1aVLF40YMULPPvusqlat+pfqyMvLU2hoqNatW1dsXUBAwF8aG5XHrl27FBUVZS37+Pg4rc/Ly9Njjz2m2267rdi23t7eOnjwoHr06KF77rlH06ZNU2BgoD777DMNGTJEp0+ftoKTh4eH07ZnP+H35zY+5Yc/n4Pnc3Y+3dKlS3XVVVc5rfvzF/3+eezJkyfrH//4h5YuXaply5Zp0qRJevPNN/W3v/1NeXl5uvvuuzV69Ohi+6xbt671Z87j8yM44YI4HA65ubnp5MmTJa6vVauWEhISlJCQoJtuukkPPPCAnn32WTVr1kz/93//p6NHj5Z41Sk6OlobN25UQkKC1bZx40Y1btxYknTdddcpIyND7u7uioyMvCjHhsvbmjVr9PXXX2vs2LGl9rnuuuu0Z88ep/8M/FFaWpqKioo0ffp0ubn9/tmZt99++6LUi8uXn5+fwsLCtHHjRnXo0MFq37hxo9PVpD/z9/dXcHCwtm7dqptvvlnS71c8t2/frhYtWkiSGjduLC8vLx0+fNhpbLuuueYaXXPNNRo7dqz69++v1NRU/e1vf9N1112n7777rtRzH/YRnHBO+fn5ysjIkPT7rbrnnntOeXl56tmzZ7G+EydOVKtWrdSkSRPl5+dryZIl1uXf/v3764knnlCfPn2UlJSk0NBQffnllwoLC1NMTIweeOAB3XHHHWrZsqViY2P10Ucf6f3339eqVaskSbGxsYqJiVGfPn309NNP65prrlF6erqWLl2qv/3tb8UuWaNyO3teFhYWKjMzU8uXL1dSUpJ69Oihu+66q9TtJk6cqB49eqhu3bq6/fbb5ebmpp07d+qbb77R1KlTVb9+fRUUFGjOnDnq2bOnNm7cqHnz5l3CI8Pl4oEHHtCkSZNUr149tWjRQqmpqdqxY4fTrbCSjBo1SklJSapfv74aNWqkOXPm6LfffrNuMVevXl3/+c9/NHbsWBUVFal9+/bKycnRxo0b5efn5/Sfyz86efKkHnjgAd1+++2KiorS//73P23dulV9+/aVJI0fP1433HCDRo4cqaFDh8rHx0ffffedVq5cqeeee65835xKjuCEc1q+fLlCQ0Ml/f4XulGjRnrnnXfUsWNHHTx40Kmvp6enEhMTdfDgQVWtWlU33XST3nzzTWvdihUrdP/996tbt246c+aMGjdurJSUFElSnz59NGvWLD377LO67777FBUVpdTUVHXs2FHS71e6Pv74Yz3yyCMaPHiwfv75Z4WEhOjmm29WcHDwJXs/4BrOnpfu7u6qUaOGmjdvrtmzZyshIcG6UlSSuLg4LVmyRFOmTNFTTz0lDw8PNWrUyLrt3Lx5c82YMUNPPfWUEhMTdfPNNyspKemcYQxXptGjRysnJ0f333+/srKy1LhxY3344Ydq0KDBObcbP368MjIydNddd6lKlSoaPny44uLiVKVKFavP448/rlq1aikpKUk//PCDAgICrMdulKZKlSr69ddfdddddykzM1M1a9bUbbfdpscee0zS73OX1q9fr0ceeUQ33XSTjDGqV6+e7rzzzvJ5Q64gDmOMqegiAAC4EhUVFSk6Olp33HGHHn/88YouBzZwxQkAgEvk0KFDWrFihTp06KD8/Hw999xzOnDggP7xj39UdGmwiSeHAwBwibi5uWn+/Plq06aN2rVrp6+//lqrVq2y5oPC9XGrDgAAwCauOAEAANhEcAIAALCJ4AQAAGATwQkAAMAmghMAAIBNBCcAAACbCE4AAAA2EZwAAABsIjgBAADYRHACAACwieAE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byUmJkqSampqdOONN+qNN97wWXE7duzQhAkTNH78eElS7969tX79eu3atUvS/642LVmyRE8//bQmTJggSVqzZo3sdrs2b96szMzMVtd1u91yu93Gtsvl8lnNAAAgeLXrilNiYqKqq6v1/vvvKycnRzk5Ofrggw9UXV2ta665xmfF3X777SorK9OXX34pSfrss8/06aefauzYsZKko0ePyuFwKDU11djHZrNp+PDhqqiouOC6BQUFstlsRjsf/gAAAH5Om4LTtm3bdMMNN8jlcslisejXv/61Zs2apVmzZmnYsGEaMGCA/va3v/msuKeeekqZmZnq37+/wsPDNXjwYOXk5CgrK0uS5HA4JEl2u91rP7vdboy1Ji8vT06n02g1NTU+qxkAAASvNt2qW7JkiaZNm6aoqKgWYzabTY8//rgWL16sX/3qVz4p7s0339TatWu1bt06DRgwQHv37lVOTo4SEhI0efLkdq9rtVpltVp9UiMAALhytOmK02effab09PQLjo8ZM0ZVVVWXXNR5f/jDH4yrTjfddJMefvhhzZ49WwUFBZKkuLg4SVJtba3XfrW1tcYYAACAr7QpONXW1rb6GoLzwsLC9J///OeSizrv+++/V0iId4mhoaHGKw+SkpIUFxensrIyY9zlcqmyslIpKSk+qwMAAEBq4626X/7yl9q3b5+uv/76Vsc///xzxcfH+6QwSbrnnnv0pz/9Sb169dKAAQP0j3/8Q4sXL9aUKVMkSRaLRTk5OXruuefUt29fJSUlKT8/XwkJCZo4caLP6gAAAJDaGJzGjRun/Px8paenKyIiwmvshx9+0Pz583X33Xf7rLhly5YpPz9fv//973Xq1CklJCTo8ccf17x584w5c+bM0enTpzV9+nTV19drxIgRKi0tbVEfAADApbJ4fvwa7ouora3VLbfcotDQUGVnZ6tfv36SpIMHD6qwsFDnzp1TdXV1i2+5BTqXyyWbzSan09nqg++XqvdT7/t8TXR+x14Y7+8SODfRqkA4N4GO1JYc0KYrTna7XTt27NCMGTOUl5dn/PSJxWJRWlqaCgsLO11oAgAAMKvNbw6/9tpr9cEHH+jbb7/VkSNH5PF41LdvX/Xo0eNy1AcAABAw2vWTK5LUo0cPDRs2zJe1AAAABLR2/eQKAADAlYjgBAAAYBLBCQAAwCSCEwAAgEkEJwAA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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "fig, axs = plt.subplots(3, 1, figsize=(6, 10))\n", + "\n", + "axs[0].bar(species_distribution.index, species_distribution.values)\n", + "axs[0].set_ylabel(\"Count\")\n", + "axs[0].set_title(\"Distribution of Species\")\n", + "\n", + "axs[1].bar(island_distribution.index, island_distribution.values)\n", + "axs[1].set_ylabel(\"Count\")\n", + "axs[1].set_title(\"Distribution of Island\")\n", + "\n", + "axs[2].bar(sex_distribution.index, sex_distribution.values)\n", + "axs[2].set_ylabel(\"Count\")\n", + "axs[2].set_title(\"Distribution of Sex\")\n", + "\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "b04c8fae-35b4-4d8e-8fff-decee050af3a", + "metadata": {}, + "source": [ + "Let's visualize the distribution of numerical columns.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 653, + "id": "707cc972", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAxUAAAJOCAYAAADBIyqKAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy81sbWrAAAACXBIWXMAAA9hAAAPYQGoP6dpAACClklEQVR4nO3deVwVZf//8TfIqmyuIIJIarhbYbeSu6JIZpp2p2WFZnlXaqmtVObSglmp5e1WmZbmbelXzTI1U8S7UlOKXEpTc7tVsFsDXNHg+v3hj3N7BBQ4B84BXs/H4zwezjVzZj7XzOF8/JyZucbFGGMEAAAAAMXk6ugAAAAAAJRtFBUAAAAAbEJRAQAAAMAmFBUAAAAAbEJRAQAAAMAmFBUAAAAAbEJRAQAAAMAmFBUAAAAAbEJRAQAAAMAmFBUOMG7cOLm4uJTKtjp16qROnTpZpjds2CAXFxctWbKkVLY/aNAg1atXr1S2VVxnzpzRww8/rKCgILm4uGjkyJGlst3cY7Fhw4ZS2V5JKUv9cHFx0fDhwx0dBlAo5ArnQq4oGfPmzZOLi4sOHjzokO0PGjRIPj4+Dtl2eUNRYaPcP4bcl5eXl4KDgxUTE6N3331Xp0+ftst2jh07pnHjxiklJcUu67MnZ46tMF5//XXNmzdPjz32mObPn68HHnjA0SHBBt9//73GjRun9PR0R4cCWJArnDu2wiBXlF3nzp3TuHHjym1h5izcHB1AeTFhwgSFh4fr0qVLSk1N1YYNGzRy5EhNnjxZK1asUIsWLSzLvvTSS3r++eeLtP5jx45p/Pjxqlevnm666aZCv+/rr78u0naK41qxvf/++8rJySnxGGyxfv16tWnTRmPHjnV0KLCD77//XuPHj9egQYMUEBDg6HAAK+QKcgVK37lz5zR+/HhJsjojB/uiqLCT2NhYtWrVyjIdHx+v9evX64477tCdd96pX3/9Vd7e3pIkNzc3ubmV7K4/d+6cKleuLA8PjxLdzvW4u7s7dPuFceLECTVp0sTRYQCoAMgV+SNXAGUflz+VoC5dumjMmDE6dOiQFixYYGnP7zrZtWvXql27dgoICJCPj48iIiL0wgsvSLp8PeWtt94qSRo8eLDl9Pm8efMkXa66mzVrpuTkZHXo0EGVK1e2vPfq62RzZWdn64UXXlBQUJCqVKmiO++8U0eOHLFapl69eho0aFCe9165zuvFlt91smfPntVTTz2l0NBQeXp6KiIiQm+99ZaMMVbL5V7/vnz5cjVr1kyenp5q2rSpVq9enf8Ov8qJEyc0ZMgQBQYGysvLSy1bttRHH31kmZ97neqBAwe0cuVKS+zXu65zwYIF+tvf/qbKlSuratWq6tChg9WvfC4uLho3blye9xW0P6+Ueyy3b9+ujh07qnLlymrQoIHluuakpCS1bt1a3t7eioiI0DfffJNnHUePHtVDDz2kwMBAyz778MMPrZbJ7ftnn32m1157TSEhIfLy8lLXrl21b9++a8ZYWFu2bFGPHj3k7++vypUrq2PHjvruu++slsn9W9i3b5/lzIK/v78GDx6sc+fOWS17/vx5PfHEE6pRo4Z8fX1155136ujRo1b7e9y4cXrmmWckSeHh4QUe0+J+pnIdPHhQLi4ueuuttzR9+nTdcMMNqly5srp3764jR47IGKNXXnlFISEh8vb2Vu/evXXq1CmrddSrV0933HGHNmzYoFatWsnb21vNmze3nJ5funSpmjdvLi8vL0VGRuqnn34qUowoO8gV5IorledcsWvXLnXp0kXe3t4KCQnRq6++WuAZqlWrVql9+/aqUqWKfH191bNnT+3atctqmdz7IX7//XfFxMSoSpUqCg4O1oQJEyyfk4MHD6pmzZqSpPHjx1uO39X7/ujRo+rTp498fHxUs2ZNPf3008rOzi5S/2w9Lrl/87/99pvuv/9++fv7q2bNmhozZoyMMTpy5Ih69+4tPz8/BQUF6e233y5SfCWNoqKE5V5zea1Ty7t27dIdd9yhrKwsTZgwQW+//bbuvPNOy3/AGjdurAkTJkiShg4dqvnz52v+/Pnq0KGDZR0nT55UbGysbrrpJk2dOlWdO3e+ZlyvvfaaVq5cqeeee05PPPGE1q5dq+joaJ0/f75I/StMbFcyxujOO+/UlClT1KNHD02ePFkRERF65plnNHr06DzLf/vtt3r88cc1YMAATZo0SRcuXFC/fv108uTJa8Z1/vx5derUSfPnz9fAgQP15ptvyt/fX4MGDdI777xjiX3+/PmqUaOGbrrpJkvsuV8++Rk/frweeOABubu7a8KECRo/frxCQ0O1fv36wu6y6/rzzz91xx13qHXr1po0aZI8PT01YMAAffrppxowYIBuv/12TZw4UWfPntXdd99tdS12Wlqa2rRpo2+++UbDhw/XO++8owYNGmjIkCGaOnVqnm1NnDhRy5Yt09NPP634+Hht3rxZAwcOtLkP69evV4cOHZSZmamxY8fq9ddfV3p6urp06aIffvghz/L33HOPTp8+rYSEBN1zzz2aN2+e5VR1rkGDBmnatGm6/fbb9cYbb8jb21s9e/a0WqZv37669957JUlTpkzJ95gW9zOVn08++UQzZszQiBEj9NRTTykpKUn33HOPXnrpJa1evVrPPfechg4dqi+++EJPP/10nvfv27dP9913n3r16qWEhAT9+eef6tWrlz755BONGjVK999/v8aPH6/9+/frnnvucfrLQ1B85Apr5IrrK2u5IjU1VZ07d1ZKSoqef/55jRw5Uh9//LFlP19p/vz56tmzp3x8fPTGG29ozJgx+uWXX9SuXbs8xVx2drZ69OihwMBATZo0SZGRkRo7dqzlMrWaNWtq5syZkqS77rrLcvz69u1rtY6YmBhVr15db731ljp27Ki3335b7733XpH6KNl2XHL1799fOTk5mjhxolq3bq1XX31VU6dOVbdu3VSnTh298cYbatCggZ5++mlt3LixyDGWGAObzJ0710gyW7duLXAZf39/c/PNN1umx44da67c9VOmTDGSzB9//FHgOrZu3Wokmblz5+aZ17FjRyPJzJo1K995HTt2tEwnJiYaSaZOnTomMzPT0v7ZZ58ZSeadd96xtIWFhZm4uLjrrvNascXFxZmwsDDL9PLly40k8+qrr1otd/fddxsXFxezb98+S5sk4+HhYdX2888/G0lm2rRpebZ1palTpxpJZsGCBZa2ixcvmqioKOPj42PV97CwMNOzZ89rrs8YY/bu3WtcXV3NXXfdZbKzs63m5eTkWMU9duzYPO+/en/mHovExERLW+6xXLhwoaVt9+7dRpJxdXU1mzdvtrSvWbMmz34fMmSIqV27tvnvf/9rte0BAwYYf39/c+7cOattN27c2GRlZVmWe+edd4wks2PHjuvuj4L6kZOTYxo2bGhiYmKs9su5c+dMeHi46datm6Ut92/hoYceslrnXXfdZapXr26ZTk5ONpLMyJEjrZYbNGhQnv395ptvGknmwIEDeWK15TN1pQMHDhhJpmbNmiY9Pd3SHh8fbySZli1bmkuXLlna7733XuPh4WEuXLhgaQsLCzOSzPfff29pyz2m3t7e5tChQ5b22bNn5/msoGwhV5ArjKnYuWLkyJFGktmyZYul7cSJE8bf39/qO/v06dMmICDAPPLII1bvT01NNf7+/lbtcXFxRpIZMWKEpS0nJ8f07NnTeHh4WP5W/vjjjwL3d+46JkyYYNV+8803m8jIyEL3zxjbj0vu3/zQoUMtbX/99ZcJCQkxLi4uZuLEiZb2P//803h7e+f7t+conKkoBT4+Ptcc2SP3ZtLPP/+82L9Eenp6avDgwYVe/sEHH5Svr69l+u6771bt2rX11VdfFWv7hfXVV1+pUqVKeuKJJ6zan3rqKRljtGrVKqv26Oho1a9f3zLdokUL+fn56ffff7/udoKCgiy/WkuXr9l94okndObMGSUlJRU59uXLlysnJ0cvv/yyXF2t/3TsOeyjj4+PBgwYYJmOiIhQQECAGjdurNatW1vac/+duy+MMfq///s/9erVS8YY/fe//7W8YmJilJGRoR9//NFqW4MHD7a6lrp9+/ZW6yyOlJQU7d27V/fdd59OnjxpieHs2bPq2rWrNm7cmOdz/uijj1pNt2/fXidPnlRmZqYkWS5jePzxx62WGzFiRJHjK+5nKj9///vf5e/vb5nOPSb333+/1bXwrVu31sWLF3X06FGr9zdp0kRRUVF53t+lSxfVrVs3T7stxwXOj1zxP+SK6ytrueKrr75SmzZt9Le//c3SVrNmzTxnPNauXav09HTde++9VrFVqlRJrVu3VmJiYp51XzlUeO7lcBcvXsz3sq+C5JeHivOdW9zjcqWHH37Y8u9KlSqpVatWMsZoyJAhlvaAgABFREQ4VV7gRu1ScObMGdWqVavA+f3799cHH3yghx9+WM8//7y6du2qvn376u67787zhVSQOnXqFOlGu4YNG1pNu7i4qEGDBiU+TvShQ4cUHBxslaSky6eXc+df6cr/WOWqWrWq/vzzz+tup2HDhnn2X0HbKYz9+/fL1dW1xG/UCwkJyZN4/P39FRoamqdNkmVf/PHHH0pPT9d7771X4CnbEydOWE1fvX+rVq1qtc7i2Lt3ryQpLi6uwGUyMjIs27peHH5+fjp06JBcXV0VHh5utVyDBg2KHF9xP1OFWVfuMbnesbLX+1G+kCv+h1xxfWUtVxw6dMjqP9W5IiIirKZzc0iXLl3yXY+fn5/VtKurq2644QarthtvvFGSCv059fLyynM5W3HzQnGPy5Xyyw1eXl6qUaNGnvbiXLpbUigqSth//vMfZWRkXPM/P97e3tq4caMSExO1cuVKrV69Wp9++qm6dOmir7/+WpUqVbrudnJHC7Gngn5Ryc7OLlRM9lDQdsxVN+qVBYW94augPl9vX+T+cnn//fcX+B/6K4erLMw6iyM3jjfffLPAIS2vftBQaR5ne26ruMfKXu9H+UGusE15+pupKLmiILnxzZ8/X0FBQXnml8SIaPb8nNrjez2/ZcvCZ5yiooTNnz9fkhQTE3PN5VxdXdW1a1d17dpVkydP1uuvv64XX3xRiYmJio6OtvtTVXN/CchljNG+ffusvkiqVq2a7wPEDh06ZPWrQFFiCwsL0zfffKPTp09b/QK1e/duy3x7CAsL0/bt25WTk2P1C5Qt26lfv75ycnL0yy+/XHP89/z228WLF3X8+PEib7MoatasKV9fX2VnZys6OrpEt3UtuZcg+Pn52S2OsLAw5eTk6MCBA1a/nOY3+khpPYEYsCdyhTVyRclxVK4ICwvL83mSpD179lhN5+aQWrVqFSq+nJwc/f7775azE5L022+/SZJlRDHyQungnooStH79er3yyisKDw+/5igJVw81KcnyRZSVlSVJqlKliiTZ7SnBH3/8sdW1u0uWLNHx48cVGxtraatfv742b96sixcvWtq+/PLLPMMJFiW222+/XdnZ2frnP/9p1T5lyhS5uLhYbd8Wt99+u1JTU/Xpp59a2v766y9NmzZNPj4+6tixY5HX2adPH7m6umrChAl5rme+8peC+vXr5xmN4b333ivy0HRFValSJfXr10//93//p507d+aZ/8cff5To9nNFRkaqfv36euutt3TmzBm7xJH7H60ZM2ZYtU+bNi3Psvb+WwFKGrkiL3JFyXFUrrj99tu1efNmqxEA//jjD33yySdWy8XExMjPz0+vv/66Ll26VKj4rvycGGP0z3/+U+7u7urataskqXLlypLICyWNMxV2smrVKu3evVt//fWX0tLStH79eq1du1ZhYWFasWKFvLy8CnzvhAkTtHHjRvXs2VNhYWE6ceKEZsyYoZCQELVr107S5S+fgIAAzZo1S76+vqpSpYpat26d5xrzwqpWrZratWunwYMHKy0tTVOnTlWDBg30yCOPWJZ5+OGHtWTJEvXo0UP33HOP9u/frwULFljdDFfU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vn177dy5s8wfs2v1zdfXt8wes99//10zZ87U6NGj9cILL2jr1q164okn5OHhobi4OMuxufoJ02XluOHa3njjDbm5uemJJ55wdChO58SJEzpz5owmTpyoV199VW+88YZWr16tvn37KjExUR07dnR0iA41bdo0DR06VCEhIXJzc5Orq6vef/99dejQwdGhlbqcnByNHDlSbdu2VbNmzSSV/f+nOAJFhROIiIhQSkqKMjIytGTJEsXFxSkpKUlNmjTR0KFDLcs1b95ctWvXVteuXbV//37Vr1/fgVFf25EjR/Tkk09q7dq15e7a1cL0rawet9jYWMu/W7RoodatWyssLEyfffaZvL29HRiZ7a7VtyFDhpTZY5aTk6NWrVrp9ddflyTdfPPN2rlzp2bNmqW4uDgHR4eSlJycrHfeeUc//vijXFxcHB2O08nJyZEk9e7dW6NGjZIk3XTTTfr+++81a9Ysiopp07R582atWLFCYWFh2rhxo4YNG6bg4OACz8CXV8OGDdPOnTvz3GeHouHyJyfg4eGhBg0aKDIyUgkJCWrZsqXeeeedfJdt3bq1JGnfvn2lGWKRJScn68SJE7rlllvk5uYmNzc3JSUl6d1335Wbm5sCAwN18eJFpaenW70vLS1NQUFBjgm6kK7Xt+zs7DzvKSvH7WoBAQG68cYbtW/fPgUFBZXZY5afK/uWn7JyzGrXrm05s5mrcePGlku7co/N1SOWlNXjhv/597//rRMnTqhu3bqW76JDhw7pqaeeUr169RwdnsPVqFFDbm5u1/z7qKjOnz+vF154QZMnT1avXr3UokULDR8+XP3799dbb73l6PBK1fDhw/Xll18qMTFRISEhlvbylvNKA0WFE8rJyVFWVla+81JSUiRd/o+EM+vatat27NihlJQUy6tVq1YaOHCg5d/u7u5at26d5T179uzR4cOHre4ncUbX61ulSpXyvKesHLernTlzRvv371ft2rUVGRlZZo9Zfq7sW37KyjFr27ZtnmEQf/vtN4WFhUmSwsPDFRQUZHXcMjMztWXLljJ53PA/DzzwgLZv3271XRQcHKxnnnlGa9ascXR4Dufh4aFbb731mn8fFdWlS5d06dIlubpa/zewUqVKljM85Z0xRsOHD9eyZcu0fv16hYeHW80vbzmvNHD5k4PFx8crNjZWdevW1enTp7Vw4UJt2LBBa9as0f79+7Vw4ULdfvvtql69urZv365Ro0apQ4cO+Q5h6kx8fX0t1yXmqlKliqpXr25pHzJkiEaPHq1q1arJz89PI0aMUFRUlNq0aeOIkAvten0ry8ft6aefVq9evRQWFqZjx45p7NixqlSpku699175+/uX2WMmXbtvZfmYjRo1Srfddptef/113XPPPfrhhx/03nvv6b333pMkyzNUXn31VTVs2FDh4eEaM2aMgoOD1adPH8cGj+s6c+aM1dmyAwcOKCUlRdWqVVPdunVVvXp1q+Xd3d0VFBSkiIiI0g7VIa63f5555hn1799fHTp0UOfOnbV69Wp98cUX2rBhg+OCLiXX2zcdO3bUM888I29vb4WFhSkpKUkff/yxJk+e7MCoS8+wYcO0cOFCff755/L19bXcJ+Hv7y9vb+8yn/McwtHDT1V0Dz30kAkLCzMeHh6mZs2apmvXrubrr782xhhz+PBh06FDB1OtWjXj6elpGjRoYJ555hmTkZHh4KiL5+ohO8+fP28ef/xxU7VqVVO5cmVz1113mePHjzsuQBtc2beyfNz69+9vateubTw8PEydOnVM//79zb59+yzzy/Ixu1bfyvIxM8aYL774wjRr1sx4enqaRo0amffee89qfk5OjhkzZowJDAw0np6epmvXrmbPnj0OihZFkZiYaCTlecXFxeW7fEUbUrYw+2fOnDmmQYMGxsvLy7Rs2dIsX77ccQGXouvtm+PHj5tBgwaZ4OBg4+XlZSIiIszbb79tcnJyHBt4Kclv30gyc+fOtSxTlnOeI7gYY0ypVjEAAAAAyhXuqQAAAABgE4oKAAAAADahqAAAAABgE4oKAAAAADahqAAAAABgE4oKAAAAADahqAAAAABgE4oKAAAAADahqAAAAABgE4oKAAAAADahqAAAAABgE4oKAAAAADahqAAAAABgE4oKAAAAADahqAAAAABgE4oKAAAAADahqAAAAABgE4oKAAAAADahqHAC48aNk4uLS6lsq1OnTurUqZNlesOGDXJxcdGSJUtKZfuDBg1SvXr1SmVbxXXmzBk9/PDDCgoKkouLi0aOHFngsn/99ZeeffZZhYaGytXVVX369JEkubi4aNy4cZbl5s2bJxcXFx08eLBEYy8NuZ+ZDRs2ODqU63JxcdHw4cMdHQbgdMg7zqUoeae0vtfKU95C6aCosLPcP8Lcl5eXl4KDgxUTE6N3331Xp0+ftst2jh07pnHjxiklJcUu67MnZ46tMF5//XXNmzdPjz32mObPn68HHnigwGU//PBDvfnmm7r77rv10UcfadSoUaUYKSTp+++/17hx45Senu7oUACHIO84d2yFUZS8AzgrN0cHUF5NmDBB4eHhunTpklJTU7VhwwaNHDlSkydP1ooVK9SiRQvLsi+99JKef/75Iq3/2LFjGj9+vOrVq6ebbrqp0O/7+uuvi7Sd4rhWbO+//75ycnJKPAZbrF+/Xm3atNHYsWMLtWydOnU0ZcqUay73wAMPaMCAAfL09LRXmPj/vv/+e40fP16DBg1SQECAo8MBHIa8UzHyDuCsKCpKSGxsrFq1amWZjo+P1/r163XHHXfozjvv1K+//ipvb29Jkpubm9zcSvZQnDt3TpUrV5aHh0eJbud63N3dHbr9wjhx4oSaNGlS6GUL8x/ZSpUqqVKlSjZGVrLOnj2rKlWqODoMAMVE3slfecs7gLPi8qdS1KVLF40ZM0aHDh3SggULLO35Xdu6du1atWvXTgEBAfLx8VFERIReeOEFSZevR7311lslSYMHD7ac8p43b56ky9evNmvWTMnJyerQoYMqV65see/V17bmys7O1gsvvKCgoCBVqVJFd955p44cOWK1TL169TRo0KA8771yndeLLb9rW8+ePaunnnpKoaGh8vT0VEREhN566y0ZY6yWy72OdPny5WrWrJk8PT3VtGlTrV69Ov8dfpUTJ05oyJAhCgwMlJeXl1q2bKmPPvrIMj/3Ot8DBw5o5cqVltjzu5704MGDcnFxUWJionbt2mVZtqD7DPK7NrVevXq644479PXXX+umm26Sl5eXmjRpoqVLl+b73o0bN+of//iHqlevLj8/Pz344IP6888/82xr1apVat++vapUqSJfX1/17NlTu3btslpm0KBB8vHx0f79+3X77bfL19dXAwcOLNR+LMiWLVvUo0cP+fv7q3LlyurYsaO+++47q2VyP+v79u2znFnw9/fX4MGDde7cOatlz58/ryeeeEI1atSQr6+v7rzzTh09etTqfpVx48bpmWeekSSFh4cXeMyK+5nJlXu833rrLU2fPl033HCDKleurO7du+vIkSMyxuiVV15RSEiIvL291bt3b506dcpqHbnHe8OGDWrVqpW8vb3VvHlzy2dm6dKlat68uby8vBQZGamffvqpSDEC+SHvlJ+8c7VPPvlEERERlu+MjRs35lnmp59+UmxsrPz8/OTj46OuXbtq8+bNeZbbtWuXunTpIm9vb4WEhOjVV1/Nc3YnLi5ONWrU0KVLl/K8v3v37oqIiCjEHrksNwcdPnxYd9xxh3x8fFSnTh1Nnz5dkrRjxw516dJFVapUUVhYmBYuXGj1/lOnTunpp59W8+bN5ePjIz8/P8XGxurnn3/Os61p06apadOmqly5sqpWrapWrVpZre/06dMaOXKk6tWrJ09PT9WqVUvdunXTjz/+WOj+SNL27dvVsWNHq304d+7cCnVfCmcqStkDDzygF154QV9//bUeeeSRfJfZtWuX7rjjDrVo0UITJkyQp6en9u3bZ/kPWuPGjTVhwgS9/PLLGjp0qNq3by9Juu222yzrOHnypGJjYzVgwADdf//9CgwMvGZcr732mlxcXPTcc8/pxIkTmjp1qqKjo5WSkmL5ZaswChPblYwxuvPOO5WYmKghQ4bopptu0po1a/TMM8/o6NGjeS4r+vbbb7V06VI9/vjj8vX11bvvvqt+/frp8OHDql69eoFxnT9/Xp06ddK+ffs0fPhwhYeHa/HixRo0aJDS09P15JNPqnHjxpo/f75GjRqlkJAQPfXUU5KkmjVr5llfzZo1NX/+fL322ms6c+aMEhISLP0vir1796p///569NFHFRcXp7lz5+rvf/+7Vq9erW7dulktO3z4cAUEBGjcuHHas2ePZs6cqUOHDlmSkiTNnz9fcXFxiomJ0RtvvKFz585p5syZateunX766SerxPrXX38pJiZG7dq101tvvaXKlSsXKfYrrV+/XrGxsYqMjNTYsWPl6uqquXPnqkuXLvr3v/+tv/3tb1bL33PPPQoPD1dCQoJ+/PFHffDBB6pVq5beeOMNyzKDBg3SZ599pgceeEBt2rRRUlKSevbsabWevn376rffftO//vUvTZkyRTVq1JBkfcyK+5nJzyeffKKLFy9qxIgROnXqlCZNmqR77rlHXbp00YYNG/Tcc89p3759mjZtmp5++ml9+OGHVu/ft2+f7rvvPv3jH//Q/fffr7feeku9evXSrFmz9MILL+jxxx+XJCUkJOiee+7Rnj175OrKbz+wDXnHWlnNO1dKSkrSp59+qieeeEKenp6aMWOGevTooR9++EHNmjWTdPmYtm/fXn5+fnr22Wfl7u6u2bNnq1OnTkpKSlLr1q0lSampqercubP++usvPf/886pSpYree++9PMfggQce0Mcff6w1a9bojjvusLSnpqZq/fr1Rb50Kzs7W7GxserQoYMmTZqkTz75RMOHD1eVKlX04osvauDAgerbt69mzZqlBx98UFFRUQoPD5ck/f7771q+fLn+/ve/Kzw8XGlpaZo9e7Y6duyoX375RcHBwZIuX/r2xBNP6O6779aTTz6pCxcuaPv27dqyZYvuu+8+SdKjjz6qJUuWaPjw4WrSpIlOnjypb7/9Vr/++qtuueWWQvXl6NGj6ty5s1xcXBQfH68qVarogw8+qHiXPBvY1dy5c40ks3Xr1gKX8ff3NzfffLNleuzYsebKQzFlyhQjyfzxxx8FrmPr1q1Gkpk7d26eeR07djSSzKxZs/Kd17FjR8t0YmKikWTq1KljMjMzLe2fffaZkWTeeecdS1tYWJiJi4u77jqvFVtcXJwJCwuzTC9fvtxIMq+++qrVcnfffbdxcXEx+/bts7RJMh4eHlZtP//8s5Fkpk2blmdbV5o6daqRZBYsWGBpu3jxoomKijI+Pj5WfQ8LCzM9e/a85vpydezY0TRt2jRPuyQzduxYy3Tu5+LAgQNW25Fk/u///s/SlpGRYWrXrm31+ch9b2RkpLl48aKlfdKkSUaS+fzzz40xxpw+fdoEBASYRx55xCqW1NRU4+/vb9UeFxdnJJnnn3++UP28Uu5nJjEx0RhjTE5OjmnYsKGJiYkxOTk5luXOnTtnwsPDTbdu3SxtuZ/1hx56yGqdd911l6levbplOjk52UgyI0eOtFpu0KBBefbtm2++mWff5rLlM3OlAwcOGEmmZs2aJj093dIeHx9vJJmWLVuaS5cuWdrvvfde4+HhYS5cuGBpyz3e33//vaVtzZo1RpLx9vY2hw4dsrTPnj3bah8D10LeqVh5R5KRZLZt22ZpO3TokPHy8jJ33XWXpa1Pnz7Gw8PD7N+/39J27Ngx4+vrazp06GBpGzlypJFktmzZYmk7ceKE8ff3t/puzc7ONiEhIaZ///5W8UyePNm4uLiY33//vVDxG/O/HPT6669b2v7880/j7e1tXFxczKJFiyztu3fvzvO9f+HCBZOdnW21zgMHDhhPT08zYcIES1vv3r3zzdFX8vf3N8OGDSt07PkZMWKEcXFxMT/99JOl7eTJk6ZatWoF5qfyiJ/AHMDHx+eao3HkXqP/+eefF/vmMk9PTw0ePLjQyz/44IPy9fW1TN99992qXbu2vvrqq2Jtv7C++uorVapUSU888YRV+1NPPSVjjFatWmXVHh0drfr161umW7RoIT8/P/3+++/X3U5QUJDuvfdeS5u7u7ueeOIJnTlzRklJSXboTdEFBwfrrrvuskznXtb0008/KTU11WrZoUOHWl0b/Nhjj8nNzc1yjNauXav09HTde++9+u9//2t5VapUSa1bt1ZiYmKe7T/22GM29yElJUV79+7Vfffdp5MnT1q2e/bsWXXt2lUbN27M8zl+9NFHrabbt2+vkydPKjMzU5Islxbk/nKfa8SIEUWOr7ifmfz8/e9/l7+/v2U695e++++/3+r69NatW+vixYs6evSo1fubNGmiqKioPO/v0qWL6tatm6e9ODEC+SHv/E95yDtRUVGKjIy0TNetW1e9e/fWmjVrlJ2drezsbH399dfq06ePbrjhBstytWvX1n333advv/3W8n371VdfqU2bNlZnlGvWrJnnklhXV1cNHDhQK1assPosffLJJ7rtttssZxGK4uGHH7b8OyAgQBEREapSpYruueceS3tERIQCAgKs9renp6flLG52drZOnjxpuWTvysuWAgIC9J///Edbt24tMIaAgABt2bJFx44dK3L8uVavXq2oqCirQQKqVatm82XFZQ1FhQOcOXPG6ov0av3791fbtm318MMPKzAwUAMGDNBnn31WpC/6OnXqFOnmuIYNG1pNu7i4qEGDBiV+HeChQ4cUHBycZ3/kXkZ06NAhq/Yr/+OVq2rVqvneW3D1dho2bJjnUpKCtlNaGjRokOe65htvvFGS8uz7q4+Rj4+PateubVlu7969ki7/B7VmzZpWr6+//lonTpywer+bm5tCQkJs7kPuduPi4vJs94MPPlBWVpYyMjKs3nP1caxataokWY7joUOH5OrqmidJNWjQoMjxFfczU5h15RYYoaGh+bZfvQ1b3w8UF3nnf8pD3rl630mXc8e5c+f0xx9/6I8//tC5c+fyvc+hcePGysnJsdy/khvn1fJ774MPPqjz589r2bJlkqQ9e/YoOTm5WEPgenl55bnMy9/fXyEhIXnyor+/v9X+zsnJ0ZQpU9SwYUN5enqqRo0aqlmzprZv326Vb5577jn5+Pjob3/7mxo2bKhhw4bluddv0qRJ2rlzp0JDQ/W3v/1N48aNK/IPOocOHco3PxUnZ5Vl3FNRyv7zn/8oIyPjmh80b29vbdy4UYmJiVq5cqVWr16tTz/9VF26dNHXX39dqFGEinI9amEV9KCk7OzsUhvZqKDtmKturquIcpP//PnzFRQUlGf+1SO9XPlLjz22++abbxY4zKSPj4/VdGkeR3tuq6B1FXYbtr4fKA7yjm34+/yfJk2aKDIyUgsWLNCDDz6oBQsWyMPDw+rMQmHZ8n34+uuva8yYMXrooYf0yiuvqFq1anJ1ddXIkSOtCuHGjRtrz549+vLLL7V69Wr93//9n2bMmKGXX35Z48ePl3T5Hr/27dtr2bJl+vrrr/Xmm2/qjTfe0NKlSxUbG1vkflVknKkoZfPnz5ckxcTEXHM5V1dXde3aVZMnT9Yvv/yi1157TevXr7dcwmLvJ6Hm/tqcyxijffv2Wd3YW7Vq1XwfMHb1ry1FiS0sLEzHjh3Lc1p+9+7dlvn2EBYWpr179+b51c3e2ymqffv25UlMv/32myTlGa3k6mN05swZHT9+3LJc7un5WrVqKTo6Os8rv9FX7CF3u35+fvluNzo6ushDOoaFhSknJ0cHDhywat+3b1+eZUvrqcBAWUXesVYe8s7V+066nDsqV65sOVNcuXJl7dmzJ89yu3fvlqurq+UMaW6cV8vvvdLlsxXr16/X8ePHtXDhQvXs2dNytrm0LFmyRJ07d9acOXM0YMAAde/eXdHR0fl+VqpUqaL+/ftr7ty5Onz4sHr27KnXXntNFy5csCxTu3ZtPf7441q+fLkOHDig6tWr67XXXit0PGFhYfnmp/zayjOKilK0fv16vfLKKwoPD7/mdXZXD0UpyfILcFZWliRZnidgr6cIf/zxx1ZfsEuWLNHx48etqvT69etr8+bNunjxoqXtyy+/zDMEYFFiu/3225Wdna1//vOfVu1TpkyRi4uL3X4luP3225WamqpPP/3U0vbXX39p2rRp8vHxUceOHe2ynaI6duyY5TSyJGVmZurjjz/WTTfdlOdsw3vvvWc1lN/MmTP1119/WfZRTEyM/Pz89Prrr+c75N8ff/xRIn2IjIxU/fr19dZbb+nMmTN22W7uf35mzJhh1T5t2rQ8y9r7bwEoT8g7eZWHvLNp0yareweOHDmizz//XN27d7c8F6l79+76/PPPrS4nS0tL08KFC9WuXTv5+flZ4ty8ebN++OEHy3J//PGHPvnkk3y3fe+998rFxUVPPvmkfv/9d91///3F7kdxVapUKc8PcosXL85zH9vJkyetpj08PNSkSRMZY3Tp0iVlZ2fnuTy3Vq1aCg4OtnzuCyMmJkabNm2yeqL7qVOnCtyH5RWXP5WQVatWaffu3frrr7+Ulpam9evXa+3atQoLC9OKFSvk5eVV4HsnTJigjRs3qmfPngoLC9OJEyc0Y8YMhYSEqF27dpIuf9EGBARo1qxZ8vX1VZUqVdS6deti3SglXb6hqF27dho8eLDS0tI0depUNWjQwGr4wYcfflhLlixRjx49dM8992j//v1asGCB1Q1sRY2tV69e6ty5s1588UUdPHhQLVu21Ndff63PP/9cI0eOzLPu4ho6dKhmz56tQYMGKTk5WfXq1dOSJUv03XffaerUqde81rgk3XjjjRoyZIi2bt2qwMBAffjhh0pLS9PcuXPzLHvx4kV17drVMtTojBkz1K5dO915552SLp8pmDlzph544AHdcsstGjBggGrWrKnDhw9r5cqVatu2bZ4kag+urq764IMPFBsbq6ZNm2rw4MGqU6eOjh49qsTERPn5+emLL74o0jojIyPVr18/TZ06VSdPnrQMKZt7FufKXyVzb1Z88cUXNWDAALm7u6tXr148yA8VDnmn4uSdZs2aKSYmxmpIWUmWS3ok6dVXX7U8e+Txxx+Xm5ubZs+eraysLE2aNMmy3LPPPqv58+erR48eevLJJy1DyoaFhWn79u15tl2zZk316NFDixcvVkBAQJ6hvkvDHXfcoQkTJmjw4MG67bbbtGPHDn3yySdWN6VLl5+fERQUpLZt2yowMFC//vqr/vnPf6pnz57y9fVVenq6QkJCdPfdd6tly5by8fHRN998o61bt+rtt98udDzPPvusFixYoG7dumnEiBGWIWXr1q2rU6dOVZwz6o4Ycqo8yx3aL/fl4eFhgoKCTLdu3cw777xjNYRcrquH9lu3bp3p3bu3CQ4ONh4eHiY4ONjce++95rfffrN63+eff26aNGli3NzcrIbSK2iY09x5+Q3t969//cvEx8ebWrVqGW9vb9OzZ0+rIS5zvf3226ZOnTrG09PTtG3b1mzbti3POq8V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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, axs = plt.subplots(2, 2, figsize=(8, 6))\n", + "\n", + "axs[0, 0].hist(penguins[\"culmen_length_mm\"], bins=20)\n", + "axs[0, 0].set_ylabel(\"Count\")\n", + "axs[0, 0].set_title(\"Distribution of culmen_length_mm\")\n", + "\n", + "axs[0, 1].hist(penguins[\"culmen_depth_mm\"], bins=20)\n", + "axs[0, 1].set_ylabel(\"Count\")\n", + "axs[0, 1].set_title(\"Distribution of culmen_depth_mm\")\n", + "\n", + "axs[1, 0].hist(penguins[\"flipper_length_mm\"], bins=20)\n", + "axs[1, 0].set_ylabel(\"Count\")\n", + "axs[1, 0].set_title(\"Distribution of flipper_length_mm\")\n", + "\n", + "axs[1, 1].hist(penguins[\"body_mass_g\"], bins=20)\n", + "axs[1, 1].set_ylabel(\"Count\")\n", + "axs[1, 1].set_title(\"Distribution of body_mass_g\")\n", + "\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "ef241df0-3acd-4401-a2c6-b70723d7595b", + "metadata": {}, + "source": [ + "Let's display the covariance matrix of the dataset. The \"covariance\" measures how changes in one variable are associated with changes in a second variable. In other words, the covariance measures the degree to which two variables are linearly associated.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 654, + "id": "3daf3ba1-d218-4ad4-b862-af679b91273f", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
culmen_length_mmculmen_depth_mmflipper_length_mmbody_mass_g
culmen_length_mm29.679415-2.51698450.2605882596.971151
culmen_depth_mm-2.5169843.877201-16.108849-742.660180
flipper_length_mm50.260588-16.108849197.2695019792.552037
body_mass_g2596.971151-742.6601809792.552037640316.716388
\n", + "
" + ], + "text/plain": [ + " culmen_length_mm culmen_depth_mm flipper_length_mm \\\n", + "culmen_length_mm 29.679415 -2.516984 50.260588 \n", + "culmen_depth_mm -2.516984 3.877201 -16.108849 \n", + "flipper_length_mm 50.260588 -16.108849 197.269501 \n", + "body_mass_g 2596.971151 -742.660180 9792.552037 \n", + "\n", + " body_mass_g \n", + "culmen_length_mm 2596.971151 \n", + "culmen_depth_mm -742.660180 \n", + "flipper_length_mm 9792.552037 \n", + "body_mass_g 640316.716388 " + ] + }, + "execution_count": 654, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "penguins.cov(numeric_only=True)" + ] + }, + { + "cell_type": "markdown", + "id": "9fbbe6bc-0104-4663-8c30-8f9566755739", + "metadata": {}, + "source": [ + "Here are three examples of what we get from interpreting the covariance matrix below:\n", + "\n", + "1. Penguins that weight more tend to have a larger culmen.\n", + "2. The more a penguin weights, the shallower its culmen tends to be.\n", + "3. There's a small variance between the culmen depth of penguins.\n", + "\n", + "Let's now display the correlation matrix. \"Correlation\" measures both the strength and direction of the linear relationship between two variables.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 655, + "id": "1d793e09-2cb9-47ff-a0e6-199a0f4fc1b3", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
culmen_length_mmculmen_depth_mmflipper_length_mmbody_mass_g
culmen_length_mm1.000000-0.2346350.6568560.595720
culmen_depth_mm-0.2346351.000000-0.582472-0.471339
flipper_length_mm0.656856-0.5824721.0000000.871302
body_mass_g0.595720-0.4713390.8713021.000000
\n", + "
" + ], + "text/plain": [ + " culmen_length_mm culmen_depth_mm flipper_length_mm \\\n", + "culmen_length_mm 1.000000 -0.234635 0.656856 \n", + "culmen_depth_mm -0.234635 1.000000 -0.582472 \n", + "flipper_length_mm 0.656856 -0.582472 1.000000 \n", + "body_mass_g 0.595720 -0.471339 0.871302 \n", + "\n", + " body_mass_g \n", + "culmen_length_mm 0.595720 \n", + "culmen_depth_mm -0.471339 \n", + "flipper_length_mm 0.871302 \n", + "body_mass_g 1.000000 " + ] + }, + "execution_count": 655, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "penguins.corr(numeric_only=True)" + ] + }, + { + "cell_type": "markdown", + "id": "8aec4c08-767c-4740-959c-2d76268c3513", + "metadata": {}, + "source": [ + "Here are three examples of what we get from interpreting the correlation matrix below:\n", + "\n", + "1. Penguins that weight more tend to have larger flippers.\n", + "2. Penguins with a shallower culmen tend to have larger flippers.\n", + "3. The length and depth of the culmen have a slight negative correlation.\n", + "\n", + "Let's display the distribution of species by island.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 656, + "id": "1258c99d", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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AEXMggULjCSzbdu2XNv4+vqaOnXq2KdHjx5trjycp02bZiSZ33//Pdc+tm3bZiSZBQsWZFv28MMPG0lm/vz5OS57+OGH7dPffPONkWTuvPNOk5ycbJ+/dOlSI8nMmDHDPq98+fKmc+fO1+3zWrV17tzZlC9f3j69bNkyI8mMHz/eod2TTz5pbDabOXjwoH2eJOPm5uYwb/fu3UaSmTVrVrZtXWn69OlGknnvvffs89LS0kxkZKQpUaKEw76XL1/etGjR4pr9GWNMZmam/b0ODAw0HTt2NHPmzDFHjhzJ1jbre9y6dWuH+X369DGSzO7du40xxhw+fNg4OzubCRMmOLT76aefjIuLi33+5cuXTVhYmClfvrw5e/Zstrqu3m6WvPb/ww8/GEnmo48+uu77cLWs92TKlCn2eampqaZ27dqmTJkyJi0tzRhjzKpVq4wks2LFCof1a9as6XA85UaS6du3r3368ccfN9WrV7/mOlk/n/Hx8fZ5Fy5cyNauV69exsvLy1y6dCnbfr377rsO+xUUFGRiYmLs87KOtaVLl9rnnT9/3lSqVMlIMt9888119w1FD2c4UCyVKFHimner+Pn5SfrzVG9+B1i6u7ura9eueW7/3HPPqWTJkvbpJ598UsHBwfrqq6/ytf28+uqrr+Ts7KwBAwY4zP/73/8uY4zDf46SFB0drYoVK9qna9asKR8fH/3666/X3U5QUJA6duxon+fq6qoBAwYoJSVFGzZsuOHabTabVq1apfHjx6tUqVL64IMP1LdvX5UvX17t27fP8UxA3759Hab79+9vr0+SPv30U2VmZqpdu3b6448/7K+goCCFh4frm2++kST98MMPio+P18CBA+3Hy5V15Sav/fv6+kqSVq1apQsXLtzwe+Pi4qJevXrZp93c3NSrVy+dOnVKO3bskPTn9zIkJESLFy+2t9uzZ49+/PFHPfPMMze8TT8/P/3222/atm3bDa2XNZZKks6dO6c//vhDDz74oC5cuJDtTqMSJUo41Obm5qb77rvP4fj76quvFBwcrCeffNI+z8vLK9uZLBQvBA4USykpKQ5/3K/Wvn17RUVFqXv37goMDFSHDh20dOnSGwofd9555w0NEA0PD3eYttlsqlSpkuXPKzhy5IhCQkKyvR8RERH25VcqV65ctj5KlSqls2fPXnc74eHhcnJy/LWR23by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DgAAYDkCBwAAsByBAwAAWI7AAQAALFeogWPjxo1q1aqVQkJCZLPZtGzZMvuy9PR0DRs2TDVq1JC3t7dCQkL03HPP6fjx4w59nDlzRp06dZKPj4/8/PzUrVs3+/PsAQBA0VCogeP8+fOqVauW5syZk23ZhQsXtHPnTo0cOVI7d+7Up59+qv3796t169YO7Tp16qSff/5Za9as0fLly7Vx40b17NnzVu0CAADIg0J9tHmzZs3UrFmzHJf5+vpqzZo1DvNmz56t++67T0ePHlW5cuUUFxenlStXatu2bapXr54kadasWWrevLlee+01h0/8AwAUsm8m3drtNRxuSbc2m02fffaZ2rRpk+Py9evXq2HDhjp79qz8/PwsqaE4KlZjOJKSkmSz2ezfwM2bN8vPz88eNiQpOjpaTk5O2rJlS679pKamKjk52eEFAIAkJSQkqH///qpQoYLc3d1VtmxZtWrVSmvXrs3T+vXr19eJEydy/FyW/BgzZoxq165dIH0VpmLz4W2XLl3SsGHD1LFjR/uz2hMSElSmTBmHdi4uLvL391dCQkKufU2aNEljx461tF4AQPFz+PBhRUVFyc/PT6+++qpq1Kih9PR0rVq1Sn379tW+ffuu24ebm5uCgoJuQbWO0tPT5erqesu3m1fF4gxHenq62rVrJ2OM5s2bd9P9DR8+XElJSfbXsWPHCqBKAEBx16dPH9lsNm3dulUxMTGqXLmyqlevrsGDB+v777+3t/vjjz/0xBNPyMvLS+Hh4friiy/sy9avXy+bzabExERJUmxsrPz8/LRq1SpFRESoRIkSatq0qU6cOOGwzn333Sdvb2/5+fkpKipKR44cUWxsrMaOHavdu3fLZrPJZrMpNjZW0p+XdubNm6fWrVvL29tbEyZMUEZGhrp166awsDB5enqqSpUqmjFjhsM+dunSRW3atNHYsWNVunRp+fj46P/+7/+UlpZm3RurYnCGIytsHDlyROvWrXP4JLqgoCCdOnXKof3ly5d15syZa6ZLd3d3ubu7W1YzAKD4OXPmjFauXKkJEybI29s72/Irx2OMHTtWkydP1quvvqpZs2apU6dOOnLkiPz9/XPs+8KFC3rttde0aNEiOTk56ZlnntELL7ygxYsX6/Lly2rTpo169OihDz74QGlpadq6datsNpvat2+vPXv2aOXKlfr6668lyeFSzZgxY/TKK69o+vTpcnFxUWZmpu666y599NFHCggI0HfffaeePXsqODhY7dq1s6+3du1aeXh4aP369Tp8+LC6du2qgIAATZgwoYDezeyKdODIChsHDhzQN998o4CAAIflkZGRSkxM1I4dO1S3bl1J0rp165SZman777+/MEoGABRTBw8elDFGVatWvW7bLl26qGPHjpKkiRMnaubMmdq6dauaNm2aY/v09HTNnz9fFStWlCT169dP48aNk/Tnx7snJSWpZcuW9uURERH2dUuUKCEXF5cc/5F++umn1bVrV4d5Vw4ZCAsL0+bNm7V06VKHwOHm5qZ33nlHXl5eql69usaNG6chQ4bo5ZdflpOTNRc/CjVwpKSk6ODBg/bp+Ph47dq1S/7+/goODtaTTz6pnTt3avny5crIyLCPy/D395ebm5siIiLUtGlT9ejRQ/Pnz1d6err69eunDh06cIcKAOCGGGPy3LZmzZr2r729veXj45PtjPuVvLy87GFCkoKDg+3t/f391aVLFzVp0kSNGzdWdHS02rVrp+Dg4OvWceVNE1nmzJmjd955R0ePHtXFixeVlpaWbdBprVq15OXlZZ+OjIxUSkqKjh07pvLly193u/lRqGM4tm/frjp16qhOnTqSpMGDB6tOnToaNWqU/vOf/+iLL77Qb7/9ptq1ays4ONj++u677+x9LF68WFWrVlWjRo3UvHlzNWjQQG+88UZh7RIAoJgKDw+XzWbL08DQqwdn2mw2ZWZm3lD7KwPOggULtHnzZtWvX19LlixR5cqVHcaM5ObqSz8ffvihXnjhBXXr1k2rV6/Wrl271LVrV8vHZ+RFoZ7heOSRR66ZKPOSNv39/fX+++8XZFkAgL8gf39/NWnSRHPmzNGAAQOy/TFPTEy09LkaWf+ADx8+XJGRkXr//ff1wAMPyM3NTRkZGXnqY9OmTapfv7769Oljn3fo0KFs7Xbv3q2LFy/K09NTkvT999+rRIkSKlu2bMHsTA6KxV0qAADcCnPmzFFGRobuu+8+ffLJJzpw4IDi4uI0c+ZMRUZGWrLN+Ph4DR8+XJs3b9aRI0e0evVqHThwwD6OIzQ01D7k4I8//lBqamqufYWHh2v79u1atWqVfvnlF40cOVLbtm3L1i4tLU3dunXT3r179dVXX2n06NHq16+fZeM3pCI+aBQAcBux6MmfBalChQrauXOnJkyYoL///e86ceKESpcurbp16xbIYxly4uXlpX379mnhwoU6ffq0goOD1bdvX/Xq1UuSFBMTo08//VQNGzZUYmKiFixYoC5duuTYV69evfTDDz+offv2stls6tixo/r06aMVK1Y4tGvUqJHCw8P10EMPKTU1VR07dtSYMWMs2b8sNnMjo2RuU8nJyfL19VVSUpLDbbdAUTZ319zCLkGS1Kd2n+s3wl/KpUuXFB8fr7CwMHl4eBR2ObhKly5dlJiY6PCBqddzre9pXv+GckkFAABYjsABAAAsxxgOAAD+QrIejX6rcYYDAABYjsABAAAsR+AAAACWI3AAAADLETgAAIDlCBwAAMBy3BYLALglbvXTcXkKbtHCGQ4AAK6QkJCg559/XpUqVZKHh4cCAwMVFRWlefPm6cKFCwW2nUceeUQDBw4ssP6KOs5wAADwX7/++quioqLk5+eniRMnqkaNGnJ3d9dPP/2kN954Q3feeadat25d2GUWS5zhAADgv/r06SMXFxdt375d7dq1U0REhCpUqKDHH39cX375pVq1aiVJSkxMVPfu3VW6dGn5+Pjo0Ucf1e7du+39jBkzRrVr19aiRYsUGhoqX19fdejQQefOnZP05weobdiwQTNmzJDNZpPNZtPhw4clSRs2bNB9990nd3d3BQcH68UXX9Tly5ftfaempmrAgAEqU6aMPDw81KBBgxw/gr6oIXAAACDp9OnTWr16tfr27Stvb+8c29hsNknSU089pVOnTmnFihXasWOH7rnnHjVq1Ehnzpyxtz106JCWLVum5cuXa/ny5dqwYYNeeeUVSdKMGTMUGRmpHj166MSJEzpx4oTKli2r//znP2revLnuvfde7d69W/PmzdPbb7+t8ePH2/sdOnSoPvnkEy1cuFA7d+5UpUqV1KRJE4dtF0UEDgAAJB08eFDGGFWpUsVh/h133KESJUqoRIkSGjZsmL799ltt3bpVH330kerVq6fw8HC99tpr8vPz08cff2xfLzMzU7Gxsbr77rv14IMP6tlnn9XatWslSb6+vnJzc5OXl5eCgoIUFBQkZ2dnzZ07V2XLltXs2bNVtWpVtWnTRmPHjtWUKVOUmZmp8+fPa968eXr11VfVrFkzVatWTW+++aY8PT319ttv39L360YxhgMAgGvYunWrMjMz1alTJ6Wmpmr37t1K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9/fPcTtxcXHq2bOnw7yoqCjNmDFD0p/HZUpKigICAhzaXLx4UYcOHbrp/cTtxRgjm81mn776d+Du3bu1adMm++UVScrIyNClS5d04cIFeXl56euvv9akSZO0b98+JScn6/Llyw7LJcnLy8v+u1OSAgMDFRoaqhIlSjjM43dj4fhLBY4b4e3tfUPts64lfvnll7rzzjsdll39OQBX9z1mzBg9/fTT+vLLL7VixQqNHj1aH374oZ544gmlpKSoV69eGjBgQLZtlitXzv61q6urwzKbzcbI8NuQt7e3KlWqZJ9+66235OvrqzfffFPdu3d3aNusWTMdOXJEX331ldasWaNGjRqpb9++eu211+Tp6XlTdaSkpCg4OFjr16/PtszPz++m+sbtJy4uTmFhYfbpq38HpqSkaOzYsWrbtm22dT08PHT48GG1bNlSvXv31oQJE+Tv769vv/1W3bp1U1pamj1w5PR7kN+NRcdfKnD4+PgoJCREmzZt0sMPP2yfv2nTJoezF1fz9fVVYGCgtm3bpoceekjSn+l7586dql27tiSpWrVqcnd319GjRx36zqvKlSurcuXKGjRokDp27KgFCxboiSee0D333KO9e/c6/JEBsthsNjk5OenixYs5Li9durQ6d+6szp0768EHH9SQIUP02muvqWbNmnrrrbd05syZHM9yREREaNOmTercubN93qZNm1StWjVJ0j333KOEhAS5uLgoNDTUkn3D7WHdunX66aefNGjQoFzb3HPPPdq/f3+uv+d27NihzMxMTZkyRU5Of97rsHTpUkvqhXX+UoFDkoYMGaLRo0erYsWKql27thYsWKBdu3Y5XLLISf/+/TVp0iRVqlRJVatW1axZs3T27Fn7acKSJUvqhRde0KBBg5SZmakGDRooKSlJmzZtko+Pj8Mv7itdvHhRQ4YM0ZNPPqmwsDD99ttv2rZtm2JiYiRJw4YN0wMPPKB+/fqpe/fu8vb21t69e7VmzRrNnj27YN8cFHmpqalKSEiQ9OclldmzZyslJUWtWrXK1nbUqFGqW7euqlevrtTUVC1fvtx+Ca5jx46aOHGi2rRpo0mTJik4OFg//PCDQkJCFBkZqSFDhqhdu3aqU6eOoqOj9a9//Uuffvqpvv76a0lSdHS0IiMj1aZNG02ePFmVK1fW8ePH9eWXX+qJJ57Idsocfw1Zx2dGRoZOnjyplStXatKkSWrZsqWee+65XNcbNWqUWrZsqXLlyunJJ5+Uk5OTdu/erT179mj8+PGqVKmS0tPTNWvWLLVq1UqbNm3S/Pnzb+GeoUAU9iASq109aDQjI8OMGTPG3HnnncbV1dXUqlXLrFixwr786oF3WdLT002/fv2Mj4+PKVWqlBk2bJh56qmnTIcOHextMjMzzfTp002VKlWMq6urKV26tGnSpInZsGGDMeZ/g0bPnj1rXyc1NdV06NDBlC1b1ri5uZmQkBDTr18/+4BQY4zZunWrady4sSlRooTx9vY2NWvWdBj4Wr58eTNt2jSHemvVqmVGjx6d/zcORU7nzp2NJPurZMmS5t577zUff/yxMSb7sfvyyy+biIgI4+npafz9/c3jjz9ufv31V3t/hw8fNjExMcbHx8d4eXmZevXqmS1bttiXz50711SoUMG4urqaypUrm3fffdehnuTkZNO/f38TEhJiXF1dTdmyZU2nTp3M0aNHrX8zUORceXy6uLiY0qVLm+joaPPOO++YjIwMeztJ5rPPPsu2/sqVK039+vWNp6en8fHxMffdd59544037MunTp1qgoODjaenp2nSpIl59913HX6fXv273hhjRo8ebWrVqpWtztwGt8JafDx9PmVmZioiIkLt2rXTyy+/XNjlAABQpP3lLqnk15EjR7R69Wo9/PDDSk1N1ezZsxUfH6+nn366sEsDAKDIuy2fNGoFJycnxcbG6t5771VUVJR++uknff311/Zr4gAAIHdcUgEAAJbjDAcAALAcgQMAAFiOwAEAACxH4AAAAJYjcAAAAMsROAAUqi5duqhNmzaWbyc2NpYPlgMKEYEDgCVuVZAAUDwQOAAAgOUIHAAs9/HHH6tGjRry9PRUQECAoqOjdf78+Rzbrly5Ug0aNJCfn58CAgLUsmVLHTp0yL788OHDstls+vTTT9WwYUN5eXmpVq1a2rx5s0M/sbGxKleunLy8vPTEE0/o9OnTlu4jgGsjcACw1IkTJ9SxY0f97W9/U1xcnNavX6+2bdsqt4ccnz9/XoMHD9b27du1du1aOTk56YknnlBmZqZDu5deekkvvPCCdu3apcqVK6tjx466fPmyJGnLli3q1q2b+vXrp127dqlhw4YaP3685fsKIHd8eBsAS504cUKXL19W27ZtVb58eUlSjRo1cm0fExPjMP3OO++odOnS2rt3r+6++277/BdeeEEtWrSQJI0dO1bVq1fXwYMHVbVqVc2YMUNNmzbV0KFDJUmVK1fWd999p5UrVxb07gHII85wALBUrVq11KhRI9WoUUNPPfWU3nzzTZ09ezbX9gcOHFDHjh1VoUIF+fj4KDQ0VJJ09OhRh3Y1a9a0fx0cHCxJOnXqlCQpLi5O999/v0P7yMjIgtgdAPlE4ABgKWdnZ61Zs0YrVqxQtWrVNGvWLFWpUkXx8fE5tm/VqpXOnDmjN998U1u2bNGWLVskSWlpaQ7tXF1d7V/bbDZJynbZBUDRQeAAYDmbzaaoqCiNHTtWP/zwg9zc3PTZZ59la3f69Gnt379fI0aMUKNGjRQREXHNsyG5iYiIsAeVLN9//32+6wdw8xjDAcBSW7Zs0dq1a/XYY4+pTJky2rJli37//XdFRERka1uqVCkFBATojTfeUHBwsI4ePaoXX3zxhrc5YMAARUVF6bXXXtPjjz+uVatWMX4DKGSc4QBgKR8fH23cuFHNmzdX5cqVNWLECE2ZMkXNmjXL1tbJyUkffvihduzYobvvvluDBg3Sq6++esPbfOCBB/Tmm29qxowZqlWrllavXq0RI0YUxO4AyCebye3eNAAAgALCGQ4AAGA5AgcAALAcgQMAAFiOwAEAACxH4AAAAJYjcAAAAMsROAAAgOUIHAAAwHIEDgAAYDkCBwAAsByBAwAAWO7/ARJ+/vjJiptCAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "unique_species = penguins[\"species\"].unique()\n", + "\n", + "fig, ax = plt.subplots(figsize=(6, 6))\n", + "for species in unique_species:\n", + " data = penguins[penguins[\"species\"] == species]\n", + " ax.hist(data[\"island\"], bins=5, alpha=0.5, label=species)\n", + "\n", + "ax.set_xlabel(\"Island\")\n", + "ax.set_ylabel(\"Count\")\n", + "ax.set_title(\"Distribution of Species by Island\")\n", + "ax.legend()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "d74ae740-3590-4dce-ac5a-6205975c83da", + "metadata": {}, + "source": [ + "Let's display the distribution of species by sex.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 657, + "id": "45b0a87f-028d-477f-9b65-199728c0b7ee", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "image/png": "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kULHbbfdVqSJn3Xr1rVZt1gsqlOnToHzE0rKsWPH5O/vn+f9CA4Otm7/txo1auQZo2LFivr777+veZy6deuqXDnbPx0FHaewXFxc9OKLL2rfvn06efKkli5dqlatWmn58uUaPnx4nv5Xvs+1a9dWuXLlrO/zwYMHZRiG6tatq6pVq9os+/btU0pKiqT/N0ehqM+rKOz4QUFBGjNmjN5//31VqVJFYWFhiomJKfT8DH9/f7m7u9u03X777ZJkfa3lypVT3759tXLlSmuYWbx4sVxdXfXoo49e8xheXl6aNm2ajh49qqNHjyo2Nlb16tXT7Nmz9corr0j6Z46FYRgaN25cntc7fvx4SbK+ZkmqVKmSZs2apT179sjLy0uzZs0q1OvFzenmm0aNW8Lvv/+utLQ01alTp8A+bm5u+u677/Ttt99q9erVWrt2rZYtW6aOHTtq/fr1cnBwuOZxLt/RUpIK+j/lS5cuFaqmklDQcYxScLe7n5+fevfurfDwcDVs2FDLly9XXFzcVe/6uPI9zc3NlcVi0Zo1a/J9rR4eHtdVY1HGnz59ugYMGKDPP/9c69ev18iRIxUdHa2tW7eqevXq11XHZU8++aRef/11rVy5Un369NGSJUv04IMPWs+oFFZgYKAGDRqkhx9+WLVq1dLixYs1efJkazgfO3aswsLC8t33yv8W161bJ+mfeSa///57njNGuHUQNFAmffjhh5JU4B+9y8qVK6dOnTqpU6dOevPNNzV16lS9+OKL+vbbbxUaGlriT3s8ePCgzbphGDp06JDN8z4qVqyY7+WAY8eOqVatWtb1otQWGBior7/+WmfPnrU5q3H5YVeXJ/1dr8DAQO3Zs0e5ubk2ZzVK+jjSP5dkGjdurIMHD1ovS1x28OBBBQUFWdcPHTqk3Nxc6504tWvXlmEYCgoKsp4ByM/ly0c///yzQkNDC11bYce/rFGjRmrUqJFeeuklff/992rTpo3mzZunyZMnX3W/kydP6ty5czZnNX799VdJsrnr6I477lCzZs20ePFiVa9eXcePH7dOxiyOihUrqnbt2tZneFz+vXRycirU+7R27Vq9//77ev7557V48WL1799f27ZtuylvEca1cekEZc4333yjV155RUFBQVedUX/mzJk8bU2bNpUk67Xny3/AS+pZDR988IHNvJGPP/5Yp06dUpcuXaxttWvX1tatW22eUbBq1ao8t0UWpbYHHnhAly5d0uzZs23aZ8yYIYvFYnP86/HAAw8oKSlJy5Yts7ZdvHhRb7/9tjw8PHTvvfcWecyDBw/q+PHjedpTU1OVkJCgihUr5rljJSYmxmb98ofq5dfZs2dPOTg4aOLEiXnO0hiGYb27pnnz5goKCtLMmTPzvM9XO7tT2PHT09Pz3EnTqFEjlStXzvo7eDUXL17UO++8Y13Pzs7WO++8o6pVq6pFixY2fZ944gmtX79eM2fOVOXKlQv1M9+9e7f++uuvPO3Hjh3T3r17Va9ePUn/3E3Svn17vfPOOzp16lSe/n/++af136mpqRo8eLDuuusuTZ06Ve+//75++OEHTZ069Zr14OZEvESptmbNGu3fv18XL15UcnKyvvnmG23YsEGBgYH64osvrvqkxkmTJum7775T165dFRgYqJSUFM2ZM0fVq1dX27ZtJf3zoe/t7a158+apQoUKcnd31913323zf8tFUalSJbVt21YDBw5UcnKyZs6cqTp16uipp56y9hk8eLA+/vhjde7cWY899pgOHz6sRYsW2UzOLGpt3bp1U4cOHfTiiy/q6NGjatKkidavX6/PP/9co0aNyjN2cQ0ZMkTvvPOOBgwYoMTERNWsWVMff/yxtmzZopkzZ151zkxBdu/erccff1xdunTRPffco0qVKumPP/7QwoULdfLkSc2cOTPP5YkjR46oe/fu6ty5sxISErRo0SI9/vjjatKkiaR/3rvJkycrKipKR48eVY8ePVShQgUdOXJEn332mYYMGaKxY8eqXLlymjt3rrp166amTZtq4MCB8vPz0/79+/XLL79YT/9fqbDjf/PNNxo+fLgeffRR3X777bp48aI+/PBDOTg4KDw8/Jrvjb+/v1577TUdPXpUt99+u5YtW6Zdu3bp3XffzfPQtscff1zPP/+8PvvsMw0dOrRQD3XbsGGDxo8fr+7du6tVq1by8PDQb7/9pvnz5ysrK0sTJkyw9o2JiVHbtm3VqFEjPfXUU6pVq5aSk5OVkJCg33//Xbt375YkPfPMMzp9+rS+/vprOTg4qHPnzho8eLAmT56shx56yPozwi3ELve6ANdw+fbWy4uzs7Ph6+tr3HfffcZbb71lcxvlZVfeghgfH2889NBDhr+/v+Hs7Gz4+/sbffr0MX799Veb/T7//HOj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Nm3apFdffVWS9NZbbykkJERPPfWUTp06pVOnTikgIEB//PGHHnjgAd15553avXu35s6dq9jYWE2ePNk67vPPP69PPvlECxcu1A8//KA6deooLCzM5tgASgZBA0CJOXTokAzDUL169Wzaq1SpIg8PD3l4eOi///2vNm/erO3bt2vFihVq2bKl6tatqzfeeEPe3t76+OOPrfvl5uYqLi5Od9xxh+655x498cQTio+PlyR5eXnJ2dlZ5cuXl6+vr3x9feXg4KA5c+YoICBAs2fPVv369dWjRw9NnDhR06dPV25urs6dO6e5c+fq9ddfV5cuXdSgQQO99957cnNzU2xs7A19v4BbAXM0AJhu+/btys3NVd++fZWVlaXdu3crIyNDlStXtul34cIFHT582Lpes2ZNVahQwbru5+enlJSUqx5r3759CgkJsZ45kaQ2bdooIyNDv//+u1JTU5WTk6M2bdpYtzs5Oemuu+7Svn37rvelArgCQQNAialTp44sFosOHDhg016rVi1JkpubmyQpIyNDfn5+2rhxY54xvL29rf92cnKy2WaxWJSbm1uyRQMwFZdOAJSYypUr67777tPs2bN17ty5Avs1b95cSUlJcnR0VJ06dWyWKlWqFPp4zs7OunTpkk1bcHCwEhISZBiGtW3Lli2qUKGCqlevrtq1a8vZ2Vlbtmyxbs/JydGOHTvUoEGDIrxaAIVB0ABQoubMmaOLFy+qZcuWWrZsmfbt26cDBw5o0aJF2r9/vxwcHBQaGqqQkBD16NFD69ev19GjR/X999/rxRdf1M6dOwt9rJo1a2rbtm06evSo/vrrL+Xm5mrYsGE6ceKERowYof379+vzzz/X+PHjNWbMGJUrV07u7u4aOnSonnvuOa1du1Z79+7VU089pfPnzysiIsLEdwa4NXHpBChDivsArRupdu3a+vHHHzV16lRFRUXp999/l4uLixo0aKCxY8dq2LBhslgs+uqrr/Tiiy9q4MCB+vPPP+Xr66t27drJx8en0McaO3as+vfvrwYNGujChQs6cuSIatasqa+++krPPfecmjRpokqVKikiIkIvvfSSdb9XX31Vubm5euKJJ3T27Fm1bNlS69atU8WKFc14S4BbmsX49/nFm1B6erq8vLyUlpYmT0/PEhs3akGPEhsLt4bogSsL3TczM1NHjhxRUFCQXF1dzSsKN0xxfqb8nUFRFeXvTGGUxGcol04AAIBpCBoAAMA0BA0AAGAaggYAADANQQMoxW7yudq3FH6WuFURNIBS6PITMc+fP2/nSlBSLv8sr3zaKXCz4zkaQCnk4OAgb29v6/d6lC9f3ua7O1B2GIah8+fPKyUlRd7e3nJwcLB3ScANRdAASilfX19JuuaXiKFs8Pb2tv5MgVsJQQMopSwWi/z8/FStWjXl5OTYuxxcBycnJ85k4JZF0ABKOQcHBz6kAJRZZWIyaExMjGrWrClXV1fdfffd2r59u71LAgAAhVDqg8ayZcs0ZswYjR8/Xj/88IOaNGmisLAwrlsDAFAGlPqg8eabb+qpp57SwIED1aBBA82bN0/ly5fX/Pnz7V0aAAC4hlI9RyM7O1uJiYmKioqytpUrV06hoaFKSEjId5+srCxlZWVZ19PS0iT98w10JSnrApPzUDQl/TuImx9/Z1BUJf135vJ41/PAuVIdNP766y9dunRJPj4+Nu0+Pj7av39/vvtER0dr4sSJedoDAgJMqREorBmRXvYuAcBNzqy/M2fPnpWXV/HGLtVBoziioqI0ZswY63pubq7OnDmjypUrl9gDj9LT0xUQEKATJ07I09OzRMYEAOB6mPHZZBiGzp49K39//2KPUaqDRpUqVeTg4KDk5GSb9uTk5AIffOPi4iIXFxebNm9vb1Pq8/T0JGgAAEqVkv5sKu6ZjMtK9WRQZ2dntWjRQvHx8da23NxcxcfHKyQkxI6VAQCAwijVZzQkacyYMerfv79atmypu+66SzNnztS5c+c0cOBAe5cGAACuodQHjV69eunPP//Uyy+/rKSkJDVt2lRr167NM0H0RnJxcdH48ePzXKIBAMBeSutnk8W4nntWAAAArqJUz9EAAABlG0EDAACYhqABAABMQ9AAAACmueWCxoABA2SxWPSf//wnz7bIyEhZLBYNGDDApj0hIUEODg7q2rVrnn2OHj0qi8WiXbt25Xu8uLg4WSyWPIurq2tJvBwAQBlw+bPnyuXQoUMFbuvcubN1/5o1a8piseijjz7KM3bDhg1lsVgUFxeXZ1t0dLQcHBz0+uuv59kWFxd31QdaFqauwrjlgob0z/eefPTRR7pw4YK1LTMzU0uWLFGNGjXy9I+NjdWIESP03Xff6eTJk0U+nqenp06dOmWzHDt27LpeAwCgbOncuXOez4KgoKACty1dutRm/4CAAC1YsMCmbevWrUpKSpK7u3u+x5w/f76ef/75Yn/jeWHqupZbMmg0b95cAQEB+vTTT61tn376qWrUqKFmzZrZ9M3IyNCyZcs0dOhQde3aNd/EeC0Wi0W+vr42iz2fAwIAuPFcXFzyfBY4ODgUuK1ixYo2+/ft21ebNm3SiRMnrG3z589X37595eiY97FYmzZt0oULFzRp0iSlp6fr+++/L5Gar6zrWm7JoCFJgwYNskmG8+fPz/dpo8uXL1f9+vVVr1499evXT/Pnz7+ur8sFAKA4fHx8FBYWpoULF0qSzp8/r2XLlmnQoEH59o+NjVWfPn3k5OSkPn36KDY29kaWa3XLBo1+/fpp8+bNOnbsmI4dO6YtW7aoX79+efrFxsZa2zt37qy0tDRt2rSpSMdKS0uTh4eHzdKlS5cSeR0AgLJh1apVNp8Djz76aIHbPDw8NHXq1DxjDBo0SHFxcTIMQx9//LFq166tpk2b5umXnp6ujz/+2Pr51a9fPy1fvlwZGRnXVXNBdV1NqX8EuVmqVq1qvRRiGIa6du2qKlWq2PQ5cOCAtm/frs8++0yS5OjoqF69eik2Nlbt27cv9LEqVKigH374wabNzc3tul8DAKDs6NChg+bOnWtd//e8iiu3SVKlSpXyjNG1a1c9/fTT+u677zR//vwCz2YsXbpUtWvXVpMmTSRJTZs2VWBgoJYtW6aIiIhi11xQXVdzywYN6Z9kOHz4cElSTExMnu2xsbG6ePGi/P39rW2GYcjFxUWzZ88u9FfnlitXTnXq1CmZogEAZZK7u3uBnwVX2/Zvjo6OeuKJJzR+/Hht27bN+j/CV4qNjdUvv/xiM3cjNzdX8+fPL1LQKGxdV635uvYu4zp37qzs7GxZLBaFhYXZbLt48aI++OADTZ8+Xffff7/Nth49emjp0qX53iILAICZBg0apDfeeEO9evXKd2LmTz/9pJ07d2rjxo02Zx/OnDmj9u3ba//+/apfv/4Nq/eWDhoODg7at2+f9d//tmrVKv3999+KiIjIc+YiPDxcsbGxNkHjwIEDecZv2LChpH/OgiQlJeXZXq1aNZUrd8tOkwEA/P+ysrLyfE44OjrmuaQvScHBwfrrr79Uvnz5fMeKjY3VXXfdpXbt2uXZdueddyo2Ntb6XI1Lly7leQ6Ui4uLgoODi1xXQW7poCH984yL/MTGxio0NDTfyyPh4eGaNm2a9uzZY92/d+/eefpdvgUpPT1dfn5+ebafOnVKvr6+11M+AOAmsHbt2jyfE/Xq1dP+/fvz7V+5cuV827Ozs7Vo0SL997//zXd7eHi4pk+fbp3QmZGRkeexDrVr19ahQ4eKVVd++Jp4AABgGs7bAwAA0xA0AACAaQgaAADANAQNAABgGoIGAAAwDUEDAACYhqABAABMQ9AAAACmIWgAAADTEDQAmOLPP//U0KFDVaNGDbm4uMjX11dhYWHasmWLvUsDcAPd8t91AsAc4eHhys7O1sKFC1WrVi0lJycrPj5ep0+ftndpAG4gzmgAKHGpqan6v//7P7322mvq0KGDAgMDdddddykqKkrdu3e39hk8eLCqVq0qT09PdezYUbt375b0z9kQX19f6xc/SdL3338vZ2dnxcfH2+U1ASgeggaAEufh4SEPDw+tXLlSWVlZ+fZ59NFHlZKSojVr1igxMVHNmzdXp06ddObMGVWtWlXz58/XhAkTtHPnTp09e1ZPPPGEhg8frk6dOt3gVwPgevDtrQBM8cknn+ipp57ShQsX1Lx5c917773q3bu3GjdurM2bN6tr165KSUmRi4uLdZ86dero+eef15AhQyRJkZGR+vrrr9WyZUv99NNP2rFjh01/AKUfQQOAaTIzM/V///d/2rp1q9asWaPt27fr/fff17lz5zRy5Ei5ubnZ9L9w4YLGjh2r1157zbp+xx136MSJE0pMTFSjRo3s8TIAXAeCBoAbZvDgwdqwYYOGDRumt99+Wxs3bszTx9vbW1WqVJEk/fzzz7rzzjuVk5Ojzz77TN26dbvBFQO4Xtx1AuCGadCggVauXKnmzZsrKSlJjo6OqlmzZr59s7Oz1a9fP/Xq1Uv16tXT4MGD9dNPP6latWo3tmgA14UzGgBK3OnTp/Xoo49q0KBBaty4sSpUqKCdO3dqxIgR6tq1q95//321a9dOZ8+e1bRp03T77bfr5MmTWr16tR5++GG1bNlSzz33nD7++GPt3r1bHh4euvfee+Xl5aVVq1bZ++UBKAKCBoASl5WVpQkTJmj9+vU6fPiwcnJyFBAQoEcffVQvvPCC3NzcdPbsWb344ov65JNPrLeztmvXTtHR0Tp8+LDuu+8+ffvtt2rbtq0k6ejRo2rSpIleffVVDR061M6vEEBhETQAAIBpeI4GAAAwDUEDAACYhqABAABMQ9AAAACmIWgAAADTEDQAAIBpCBoAAMA0BA0AAGAaggYAADANQQMAAJiGoAEAAEzz/wFOpz/y9WLoiQAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, ax = plt.subplots(figsize=(6, 6))\n", + "\n", + "for species in unique_species:\n", + " data = penguins[penguins[\"species\"] == species]\n", + " ax.hist(data[\"sex\"], bins=3, alpha=0.5, label=species)\n", + "\n", + "ax.set_xlabel(\"Sex\")\n", + "ax.set_ylabel(\"Count\")\n", + "ax.set_title(\"Distribution of Species by Sex\")\n", + "\n", + "ax.legend()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "587d06e0-b711-4e3d-b424-6fa611a51f94", + "metadata": { + "tags": [] + }, + "source": [ + "### Step 2 - Creating the Preprocessing Script\n", + "\n", + "The first step we need in the pipeline is a [Processing Step](https://docs.aws.amazon.com/sagemaker/latest/dg/build-and-manage-steps.html#step-type-processing) to run a script that will split and transform the data. This Processing Step will create a SageMaker Processing Job in the background, run the script, and upload the output to S3. You can use Processing Jobs to perform data preprocessing, post-processing, feature engineering, data validation, and model evaluation. Check the [ProcessingStep](https://sagemaker.readthedocs.io/en/stable/workflows/pipelines/sagemaker.workflow.pipelines.html#sagemaker.workflow.steps.ProcessingStep) SageMaker's SDK documentation for more information.\n" + ] + }, + { + "cell_type": "markdown", + "id": "7d656af1", + "metadata": {}, + "source": [ + "The first step is to create the script that will split and transform the input data.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 658, + "id": "fb6ba7c0-1bd6-4fe5-8b7f-f6cbdfd3846c", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Overwriting code/preprocessor.py\n" + ] + } + ], + "source": [ + "%%writefile {CODE_FOLDER}/preprocessor.py\n", + "#| label: preprocessing-script\n", + "#| echo: true\n", + "#| output: false\n", + "#| filename: preprocessor.py\n", + "#| code-line-numbers: true\n", + "\n", + "import os\n", + "import sys\n", + "import argparse\n", + "import json\n", + "import tarfile\n", + "import tempfile\n", + "import time\n", + "import joblib\n", + "import numpy as np\n", + "import pandas as pd\n", + "\n", + "from io import StringIO\n", + "from pathlib import Path\n", + "from sklearn.compose import ColumnTransformer, make_column_selector\n", + "from sklearn.impute import SimpleImputer\n", + "from sklearn.pipeline import Pipeline, make_pipeline\n", + "from sklearn.model_selection import train_test_split\n", + "from sklearn.preprocessing import OneHotEncoder, StandardScaler, OrdinalEncoder\n", + "\n", + "\n", + "def preprocess(base_directory):\n", + " \"\"\"\n", + " This function loads the supplied data, splits it and transforms it.\n", + " \"\"\"\n", + "\n", + " df = _read_data_from_input_csv_files(base_directory)\n", + " \n", + " target_transformer = ColumnTransformer(\n", + " transformers=[(\"species\", OrdinalEncoder(), [0])]\n", + " )\n", + " \n", + " numeric_transformer = make_pipeline(\n", + " SimpleImputer(strategy=\"mean\"),\n", + " StandardScaler()\n", + " )\n", + "\n", + " categorical_transformer = make_pipeline(\n", + " SimpleImputer(strategy=\"most_frequent\"),\n", + " OneHotEncoder()\n", + " )\n", + " \n", + " features_transformer = ColumnTransformer(\n", + " transformers=[\n", + " (\"numeric\", numeric_transformer, make_column_selector(dtype_exclude=\"object\")),\n", + " (\"categorical\", categorical_transformer, [\"island\"]),\n", + " ]\n", + " )\n", + "\n", + " df_train, df_validation, df_test = _split_data(df)\n", + "\n", + " _save_baselines(base_directory, df_train, df_test)\n", + "\n", + " y_train = target_transformer.fit_transform(np.array(df_train.species.values).reshape(-1, 1))\n", + " y_validation = target_transformer.transform(np.array(df_validation.species.values).reshape(-1, 1))\n", + " y_test = target_transformer.transform(np.array(df_test.species.values).reshape(-1, 1))\n", + " \n", + " df_train = df_train.drop(\"species\", axis=1)\n", + " df_validation = df_validation.drop(\"species\", axis=1)\n", + " df_test = df_test.drop(\"species\", axis=1)\n", + "\n", + " X_train = features_transformer.fit_transform(df_train)\n", + " X_validation = features_transformer.transform(df_validation)\n", + " X_test = features_transformer.transform(df_test)\n", + "\n", + " _save_splits(base_directory, X_train, y_train, X_validation, y_validation, X_test, y_test)\n", + " _save_model(base_directory, target_transformer, features_transformer)\n", + " \n", + "\n", + "def _read_data_from_input_csv_files(base_directory):\n", + " \"\"\"\n", + " This function reads every CSV file available and concatenates\n", + " them into a single dataframe.\n", + " \"\"\"\n", + "\n", + " input_directory = Path(base_directory) / \"input\"\n", + " files = [file for file in input_directory.glob(\"*.csv\")]\n", + " \n", + " if len(files) == 0:\n", + " raise ValueError(f\"The are no CSV files in {str(input_directory)}/\")\n", + " \n", + " raw_data = [pd.read_csv(file) for file in files]\n", + " df = pd.concat(raw_data)\n", + " \n", + " # Shuffle the data\n", + " return df.sample(frac=1, random_state=42)\n", + "\n", + "\n", + "def _split_data(df):\n", + " \"\"\"\n", + " Splits the data into three sets: train, validation and test.\n", + " \"\"\"\n", + "\n", + " df_train, temp = train_test_split(df, test_size=0.3)\n", + " df_validation, df_test = train_test_split(temp, test_size=0.5)\n", + "\n", + " return df_train, df_validation, df_test\n", + "\n", + "\n", + "def _save_baselines(base_directory, df_train, df_test):\n", + " \"\"\"\n", + " During the data and quality monitoring steps, we will need baselines\n", + " to compute constraints and statistics. This function saves the \n", + " untransformed data to disk so we can use them as baselines later.\n", + " \"\"\"\n", + "\n", + " for split, data in [(\"train\", df_train), (\"test\", df_test)]:\n", + " baseline_path = Path(base_directory) / f\"{split}-baseline\"\n", + " baseline_path.mkdir(parents=True, exist_ok=True)\n", + "\n", + " df = data.copy().dropna()\n", + "\n", + " # We want to save the header only for the train baseline\n", + " # but not for the test baseline. We'll use the test baseline\n", + " # to generate predictions later, and we can't have a header line\n", + " # because the model won't be able to make a prediction for it.\n", + " header = split == \"train\"\n", + " df.to_csv(baseline_path / f\"{split}-baseline.csv\", header=header, index=False)\n", + "\n", + "\n", + "def _save_splits(base_directory, X_train, y_train, X_validation, y_validation, X_test, y_test):\n", + " \"\"\"\n", + " This function concatenates the transformed features and the target variable, and\n", + " saves each one of the split sets to disk.\n", + " \"\"\"\n", + "\n", + " train = np.concatenate((X_train, y_train), axis=1)\n", + " validation = np.concatenate((X_validation, y_validation), axis=1)\n", + " test = np.concatenate((X_test, y_test), axis=1)\n", + "\n", + " train_path = Path(base_directory) / \"train\"\n", + " validation_path = Path(base_directory) / \"validation\"\n", + " test_path = Path(base_directory) / \"test\"\n", + "\n", + " train_path.mkdir(parents=True, exist_ok=True)\n", + " validation_path.mkdir(parents=True, exist_ok=True)\n", + " test_path.mkdir(parents=True, exist_ok=True)\n", + "\n", + " pd.DataFrame(train).to_csv(train_path / \"train.csv\", header=False, index=False)\n", + " pd.DataFrame(validation).to_csv(validation_path / \"validation.csv\", header=False, index=False)\n", + " pd.DataFrame(test).to_csv(test_path / \"test.csv\", header=False, index=False)\n", + "\n", + "\n", + "def _save_model(base_directory, target_transformer, features_transformer):\n", + " \"\"\"\n", + " This function creates a model.tar.gz file that contains the two transformation\n", + " pipelines we built to transform the data.\n", + " \"\"\"\n", + "\n", + " with tempfile.TemporaryDirectory() as directory:\n", + " joblib.dump(target_transformer, os.path.join(directory, \"target.joblib\"))\n", + " joblib.dump(features_transformer, os.path.join(directory, \"features.joblib\"))\n", + " \n", + " model_path = Path(base_directory) / \"model\"\n", + " model_path.mkdir(parents=True, exist_ok=True)\n", + " \n", + " with tarfile.open(f\"{str(model_path / 'model.tar.gz')}\", \"w:gz\") as tar:\n", + " tar.add(os.path.join(directory, \"target.joblib\"), arcname=\"target.joblib\")\n", + " tar.add(os.path.join(directory, \"features.joblib\"), arcname=\"features.joblib\")\n", + "\n", + " \n", + "if __name__ == \"__main__\":\n", + " preprocess(base_directory=\"/opt/ml/processing\")" + ] + }, + { + "cell_type": "markdown", + "id": "39301f9f", + "metadata": {}, + "source": [ + "Let's test the script to ensure everything is working as expected:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 659, + "id": "d1f122a4-acff-4687-91b9-bfef13567d88", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\n", + "\u001b[32m\u001b[32m\u001b[1m8 passed\u001b[0m\u001b[32m in 0.16s\u001b[0m\u001b[0m\n" + ] + } + ], + "source": [ + "%%ipytest -s\n", + "\n", + "#| code-fold: true\n", + "#| output: false\n", + "\n", + "import os\n", + "import shutil\n", + "import tarfile\n", + "import pytest\n", + "import tempfile\n", + "import joblib\n", + "from preprocessor import preprocess\n", + "\n", + "\n", + "@pytest.fixture(scope=\"function\", autouse=False)\n", + "def directory():\n", + " directory = tempfile.mkdtemp()\n", + " input_directory = Path(directory) / \"input\"\n", + " input_directory.mkdir(parents=True, exist_ok=True)\n", + " shutil.copy2(DATA_FILEPATH, input_directory / \"data.csv\")\n", + " \n", + " directory = Path(directory)\n", + " preprocess(base_directory=directory)\n", + " \n", + " yield directory\n", + " \n", + " shutil.rmtree(directory)\n", + "\n", + "\n", + "def test_preprocess_generates_data_splits(directory):\n", + " output_directories = os.listdir(directory)\n", + " \n", + " assert \"train\" in output_directories\n", + " assert \"validation\" in output_directories\n", + " assert \"test\" in output_directories\n", + "\n", + "\n", + "def test_preprocess_generates_baselines(directory):\n", + " output_directories = os.listdir(directory)\n", + "\n", + " assert \"train-baseline\" in output_directories\n", + " assert \"test-baseline\" in output_directories\n", + "\n", + "\n", + "def test_preprocess_creates_two_models(directory):\n", + " model_path = directory / \"model\"\n", + " tar = tarfile.open(model_path / \"model.tar.gz\", \"r:gz\")\n", + "\n", + " assert \"features.joblib\" in tar.getnames()\n", + " assert \"target.joblib\" in tar.getnames()\n", + "\n", + "\n", + "def test_splits_are_transformed(directory):\n", + " train = pd.read_csv(directory / \"train\" / \"train.csv\", header=None)\n", + " validation = pd.read_csv(directory / \"validation\" / \"validation.csv\", header=None)\n", + " test = pd.read_csv(directory / \"test\" / \"test.csv\", header=None)\n", + "\n", + " # After transforming the data, the number of features should be 7:\n", + " # * 3 - island (one-hot encoded)\n", + " # * 1 - culmen_length_mm = 1\n", + " # * 1 - culmen_depth_mm\n", + " # * 1 - flipper_length_mm\n", + " # * 1 - body_mass_g\n", + " number_of_features = 7\n", + "\n", + " # The transformed splits should have an additional column for the target\n", + " # variable.\n", + " assert train.shape[1] == number_of_features + 1\n", + " assert validation.shape[1] == number_of_features + 1\n", + " assert test.shape[1] == number_of_features + 1\n", + "\n", + "\n", + "def test_train_baseline_is_not_transformed(directory):\n", + " baseline = pd.read_csv(directory / \"train-baseline\" / \"train-baseline.csv\", header=None)\n", + "\n", + " island = baseline.iloc[:, 1].unique()\n", + "\n", + " assert \"Biscoe\" in island\n", + " assert \"Torgersen\" in island\n", + " assert \"Dream\" in island\n", + "\n", + "\n", + "def test_test_baseline_is_not_transformed(directory):\n", + " baseline = pd.read_csv(directory / \"test-baseline\" / \"test-baseline.csv\", header=None)\n", + "\n", + " island = baseline.iloc[:, 1].unique()\n", + "\n", + " assert \"Biscoe\" in island\n", + " assert \"Torgersen\" in island\n", + " assert \"Dream\" in island\n", + "\n", + "\n", + "def test_train_baseline_includes_header(directory):\n", + " baseline = pd.read_csv(directory / \"train-baseline\" / \"train-baseline.csv\")\n", + " assert baseline.columns[0] == \"species\"\n", + "\n", + "\n", + "def test_test_baseline_does_not_include_header(directory):\n", + " baseline = pd.read_csv(directory / \"test-baseline\" / \"test-baseline.csv\")\n", + " assert baseline.columns[0] != \"species\"" + ] + }, + { + "cell_type": "markdown", + "id": "dbff9c36", + "metadata": {}, + "source": [ + "### Step 3 - Setting up the Processing Step\n", + "\n", + "Let's now define the [ProcessingStep](https://sagemaker.readthedocs.io/en/stable/workflows/pipelines/sagemaker.workflow.pipelines.html#sagemaker.workflow.steps.ProcessingStep) that we'll use in the pipeline to run the script that will split and transform the data.\n" + ] + }, + { + "cell_type": "markdown", + "id": "ff061663", + "metadata": {}, + "source": [ + "Several SageMaker Pipeline steps support caching. When a step runs, and dependending on the configured caching policy, SageMaker will try to reuse the result of a previous successful run of the same step. You can find more information about this topic in [Caching Pipeline Steps](https://docs.aws.amazon.com/sagemaker/latest/dg/pipelines-caching.html). Let's define a caching policy that we'll reuse on every step:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 660, + "id": "d88e9ccf", + "metadata": {}, + "outputs": [], + "source": [ + "from sagemaker.workflow.steps import CacheConfig\n", + "\n", + "cache_config = CacheConfig(enable_caching=True, expire_after=\"15d\")" + ] + }, + { + "cell_type": "markdown", + "id": "f3b1d96a", + "metadata": {}, + "source": [ + "We can parameterize a SageMaker Pipeline to make it more flexible. In this case, we'll use a paramater to pass the location of the dataset we want to process. We can execute the pipeline with different datasets by changing the value of this parameter. To read more about these parameters, check [Pipeline Parameters](https://docs.aws.amazon.com/sagemaker/latest/dg/build-and-manage-parameters.html).\n" + ] + }, + { + "cell_type": "code", + "execution_count": 661, + "id": "331fe373", + "metadata": {}, + "outputs": [], + "source": [ + "from sagemaker.workflow.parameters import ParameterString\n", + "\n", + "dataset_location = ParameterString(\n", + " name=\"dataset_location\",\n", + " default_value=f\"{S3_LOCATION}/data\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "cfb9a589", + "metadata": {}, + "source": [ + "A processor gives the Processing Step information about the hardware and software that SageMaker should use to launch the Processing Job. To run the script we created, we need access to Scikit-Learn, so we can use the [SKLearnProcessor](https://sagemaker.readthedocs.io/en/stable/frameworks/sklearn/sagemaker.sklearn.html#scikit-learn-processor) processor that comes out-of-the-box with the SageMaker's Python SDK. The [Data Processing with Framework Processors](https://docs.aws.amazon.com/sagemaker/latest/dg/processing-job-frameworks.html) page discusses other built-in processors you can use. The [Docker Registry Paths and Example Code](https://docs.aws.amazon.com/sagemaker/latest/dg/sagemaker-algo-docker-registry-paths.html) page contains information about the available framework versions for each region.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 662, + "id": "3aa4471a", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO:sagemaker.image_uris:Defaulting to only available Python version: py3\n" + ] + } + ], + "source": [ + "from sagemaker.sklearn.processing import SKLearnProcessor\n", + "\n", + "processor = SKLearnProcessor(\n", + " base_job_name=\"split-and-transform-data\",\n", + " framework_version=\"1.2-1\",\n", + " # By default, a new account doesn't have access to `ml.m5.xlarge` instances.\n", + " # If you haven't requested a quota increase yet, you can use an\n", + " # `ml.t3.medium` instance type instead. This will work out of the box, but\n", + " # the Processing Job will take significantly longer than it should have.\n", + " # To get access to `ml.m5.xlarge` instances, you can request a quota\n", + " # increase under the Service Quotas section in your AWS account.\n", + " instance_type=config[\"instance_type\"],\n", + " instance_count=1,\n", + " role=role,\n", + " sagemaker_session=config[\"session\"],\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "6cf2cc58", + "metadata": {}, + "source": [ + "Let's now define the Processing Step that we'll use in the pipeline. This step requires a list of inputs that we need on the preprocessing script. In this case, the input is the dataset we stored in S3. We also have a few outputs that we want SageMaker to capture when the Processing Job finishes.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 663, + "id": "cdbd9303", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/svpino/dev/ml.school/.venv/lib/python3.9/site-packages/sagemaker/workflow/pipeline_context.py:297: UserWarning: Running within a PipelineSession, there will be No Wait, No Logs, and No Job being started.\n", + " warnings.warn(\n" + ] + } + ], + "source": [ + "# | code: true\n", + "# | output: false\n", + "\n", + "from sagemaker.workflow.steps import ProcessingStep\n", + "from sagemaker.processing import ProcessingInput, ProcessingOutput\n", + "\n", + "\n", + "split_and_transform_data_step = ProcessingStep(\n", + " name=\"split-and-transform-data\",\n", + " step_args=processor.run(\n", + " code=f\"{CODE_FOLDER}/preprocessor.py\",\n", + " inputs=[\n", + " ProcessingInput(\n", + " source=dataset_location, destination=\"/opt/ml/processing/input\"\n", + " ),\n", + " ],\n", + " outputs=[\n", + " ProcessingOutput(\n", + " output_name=\"train\",\n", + " source=\"/opt/ml/processing/train\",\n", + " destination=f\"{S3_LOCATION}/preprocessing/train\",\n", + " ),\n", + " ProcessingOutput(\n", + " output_name=\"validation\",\n", + " source=\"/opt/ml/processing/validation\",\n", + " destination=f\"{S3_LOCATION}/preprocessing/validation\",\n", + " ),\n", + " ProcessingOutput(\n", + " output_name=\"test\",\n", + " source=\"/opt/ml/processing/test\",\n", + " destination=f\"{S3_LOCATION}/preprocessing/test\",\n", + " ),\n", + " ProcessingOutput(\n", + " output_name=\"model\",\n", + " source=\"/opt/ml/processing/model\",\n", + " destination=f\"{S3_LOCATION}/preprocessing/model\",\n", + " ),\n", + " ProcessingOutput(\n", + " output_name=\"train-baseline\",\n", + " source=\"/opt/ml/processing/train-baseline\",\n", + " destination=f\"{S3_LOCATION}/preprocessing/train-baseline\",\n", + " ),\n", + " ProcessingOutput(\n", + " output_name=\"test-baseline\",\n", + " source=\"/opt/ml/processing/test-baseline\",\n", + " destination=f\"{S3_LOCATION}/preprocessing/test-baseline\",\n", + " ),\n", + " ],\n", + " ),\n", + " cache_config=cache_config,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "fad062cb", + "metadata": {}, + "source": [ + "### Step 4 - Creating the Pipeline\n", + "\n", + "We can now create the SageMaker Pipeline and submit its definition to the SageMaker Pipelines service to create the pipeline if it doesn't exist or update it if it does.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 664, + "id": "e140642a", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{'PipelineArn': 'arn:aws:sagemaker:us-east-1:325223348818:pipeline/session1-pipeline',\n", + " 'ResponseMetadata': {'RequestId': '02b62dd1-6de0-4723-9019-f4f72862ba5c',\n", + " 'HTTPStatusCode': 200,\n", + " 'HTTPHeaders': {'x-amzn-requestid': '02b62dd1-6de0-4723-9019-f4f72862ba5c',\n", + " 'content-type': 'application/x-amz-json-1.1',\n", + " 'content-length': '85',\n", + " 'date': 'Fri, 27 Oct 2023 14:38:36 GMT'},\n", + " 'RetryAttempts': 0}}" + ] + }, + "execution_count": 664, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# | code: true\n", + "# | output: false\n", + "\n", + "from sagemaker.workflow.pipeline import Pipeline\n", + "from sagemaker.workflow.pipeline_definition_config import PipelineDefinitionConfig\n", + "\n", + "pipeline_definition_config = PipelineDefinitionConfig(use_custom_job_prefix=True)\n", + "\n", + "session1_pipeline = Pipeline(\n", + " name=\"session1-pipeline\",\n", + " parameters=[dataset_location],\n", + " steps=[\n", + " split_and_transform_data_step,\n", + " ],\n", + " pipeline_definition_config=pipeline_definition_config,\n", + " sagemaker_session=config[\"session\"],\n", + ")\n", + "\n", + "session1_pipeline.upsert(role_arn=role)" + ] + }, + { + "cell_type": "markdown", + "id": "ff8f99c1", + "metadata": {}, + "source": [ + "We can now start the pipeline:\n" + ] + }, + { + "cell_type": "markdown", + "id": "cc01c152", + "metadata": {}, + "source": [ + "#| hide\n", + "\n", + "
Note: \n", + " The %%script cell magic is a convenient way to prevent the notebook from executing a specific cell. If you want to run the cell, comment out the line containing the %%script cell magic.\n", + "
\n" + ] + }, + { + "cell_type": "code", + "execution_count": 665, + "id": "59d1e634", + "metadata": {}, + "outputs": [], + "source": [ + "%%script false --no-raise-error\n", + "\n", + "#| eval: false\n", + "#| code: true\n", + "#| output: false\n", + "\n", + "session1_pipeline.start()" + ] + }, + { + "cell_type": "markdown", + "id": "5fa56512-f322-4d8e-ae96-598ba2366784", + "metadata": {}, + "source": [ + "### Assignments\n", + "\n", + "- Assignment 1.1 The SageMaker Pipeline we built supports running a few steps in Local Mode. The goal of this assignment is to run the pipeline on your local environment using Local Mode.\n", + "\n", + "- Assignment 1.2 For this assignment, we want to run the end-to-end pipeline in SageMaker Studio. Ensure you turn off Local Mode before doing so.\n", + "\n", + "- Assignment 1.3 The pipeline uses Random Sampling to split the dataset. Modify the code to use Stratified Sampling instead.\n", + "\n", + "- Assignment 1.4 For this assignment, we want to run a distributed Processing Job across multiple instances to capitalize the `island` column of the dataset. Your dataset will consist of 10 different files stored in S3. Set up a Processing Job using two instances. When specifying the input to the Processing Job, you must set the `ProcessingInput.s3_data_distribution_type` attribute to `ShardedByS3Key`. By doing this, SageMaker will run a cluster with two instances simultaneously, each with access to half the files.\n", + "\n", + "- Assignment 1.5 You can use [Amazon SageMaker Data Wrangler](https://aws.amazon.com/sagemaker/data-wrangler/) to complete each step of the data preparation workflow (including data selection, cleansing, exploration, visualization, and processing at scale) from a single visual interface. For this assignment, load the Data Wrangler interface and use it to build the same transformations we implemented using the Scikit-Learn Pipeline. If you have questions, open the [Penguins Data Flow](penguins.flow) included in this repository.\n" + ] + }, + { + "cell_type": "markdown", + "id": "63c190c5-52b5-4ccc-8d42-847a694b8e66", + "metadata": {}, + "source": [ + "## Session 2 - Building Models And The Training Pipeline\n", + "\n", + "This session extends the [SageMaker Pipeline](https://docs.aws.amazon.com/sagemaker/latest/dg/pipelines-sdk.html) we built in the previous session with a step to train a model. We'll explore the [Training Step](https://docs.aws.amazon.com/sagemaker/latest/dg/build-and-manage-steps.html#step-type-training) and the [Tuning Step](https://docs.aws.amazon.com/sagemaker/latest/dg/build-and-manage-steps.html#step-type-tuning).\n", + "\n", + " \"Training\"\n", + "\n", + "We'll introduce [Amazon SageMaker Experiments](https://docs.aws.amazon.com/sagemaker/latest/dg/experiments.html) and use them during training. For more information about this topic, check the [SageMaker Experiments' SDK documentation](https://sagemaker.readthedocs.io/en/v2.174.0/experiments/sagemaker.experiments.html).\n" + ] + }, + { + "cell_type": "markdown", + "id": "c8608092-7aab-4fd2-aa99-47c2db27bdb7", + "metadata": {}, + "source": [ + "### Step 1 - Creating the Training Script\n", + "\n", + "This following script is responsible for training a neural network using the train data, validating the model, and saving it so we can later use it:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 666, + "id": "d92b121d-dcb9-43e8-9ee3-3ececb583e7e", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Overwriting code/train.py\n" + ] + } + ], + "source": [ + "%%writefile {CODE_FOLDER}/train.py\n", + "#| label: training-script\n", + "#| echo: true\n", + "#| output: false\n", + "#| filename: train.py\n", + "#| code-line-numbers: true\n", + "\n", + "import os\n", + "import argparse\n", + "\n", + "import numpy as np\n", + "import pandas as pd\n", + "import tensorflow as tf\n", + "\n", + "from pathlib import Path\n", + "from sklearn.metrics import accuracy_score\n", + "\n", + "from tensorflow.keras.models import Sequential\n", + "from tensorflow.keras.layers import Dense\n", + "from tensorflow.keras.optimizers import SGD\n", + "\n", + "\n", + "def train(model_directory, train_path, validation_path, epochs=50, batch_size=32):\n", + " X_train = pd.read_csv(Path(train_path) / \"train.csv\")\n", + " y_train = X_train[X_train.columns[-1]]\n", + " X_train.drop(X_train.columns[-1], axis=1, inplace=True)\n", + " \n", + " X_validation = pd.read_csv(Path(validation_path) / \"validation.csv\")\n", + " y_validation = X_validation[X_validation.columns[-1]]\n", + " X_validation.drop(X_validation.columns[-1], axis=1, inplace=True)\n", + " \n", + " model = Sequential([\n", + " Dense(10, input_shape=(X_train.shape[1],), activation=\"relu\"),\n", + " Dense(8, activation=\"relu\"),\n", + " Dense(3, activation=\"softmax\"),\n", + " ])\n", + " \n", + " model.compile(\n", + " optimizer=SGD(learning_rate=0.01),\n", + " loss=\"sparse_categorical_crossentropy\",\n", + " metrics=[\"accuracy\"]\n", + " )\n", + "\n", + " model.fit(\n", + " X_train, \n", + " y_train, \n", + " validation_data=(X_validation, y_validation),\n", + " epochs=epochs, \n", + " batch_size=batch_size,\n", + " verbose=2,\n", + " )\n", + "\n", + " predictions = np.argmax(model.predict(X_validation), axis=-1)\n", + " print(f\"Validation accuracy: {accuracy_score(y_validation, predictions)}\")\n", + " \n", + " model_filepath = Path(model_directory) / \"001\"\n", + " model.save(model_filepath) \n", + " \n", + "\n", + "if __name__ == \"__main__\":\n", + " # Any hyperparameters provided by the training job are passed to \n", + " # the entry point as script arguments. \n", + " parser = argparse.ArgumentParser()\n", + " parser.add_argument(\"--epochs\", type=int, default=50)\n", + " parser.add_argument(\"--batch_size\", type=int, default=32)\n", + " args, _ = parser.parse_known_args()\n", + " \n", + "\n", + " train(\n", + " # This is the location where we need to save our model. SageMaker will\n", + " # create a model.tar.gz file with anything inside this directory when\n", + " # the training script finishes.\n", + " model_directory=os.environ[\"SM_MODEL_DIR\"],\n", + "\n", + " # SageMaker creates one channel for each one of the inputs to the\n", + " # Training Step.\n", + " train_path=os.environ[\"SM_CHANNEL_TRAIN\"],\n", + " validation_path=os.environ[\"SM_CHANNEL_VALIDATION\"],\n", + "\n", + " epochs=args.epochs,\n", + " batch_size=args.batch_size,\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "50f0a4fa-ce70-4882-b9f5-8253df03d890", + "metadata": {}, + "source": [ + "Let's test the script to ensure everything is working as expected:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 667, + "id": "14ea27ce-c453-4cb0-b309-dbecd732957e", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "WARNING:absl:At this time, the v2.11+ optimizer `tf.keras.optimizers.SGD` runs slowly on M1/M2 Macs, please use the legacy Keras optimizer instead, located at `tf.keras.optimizers.legacy.SGD`.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "8/8 - 0s - loss: 1.0173 - accuracy: 0.4728 - val_loss: 0.9260 - val_accuracy: 0.6078 - 230ms/epoch - 29ms/step\n", + "2/2 [==============================] - 0s 1ms/step\n", + "Validation accuracy: 0.6078431372549019\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO:tensorflow:Assets written to: /var/folders/4c/v1q3hy1x4mb5w0wpc72zl3_w0000gp/T/tmpv4apdp15/model/001/assets\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[32m.\u001b[0m\n", + "\u001b[32m\u001b[32m\u001b[1m1 passed\u001b[0m\u001b[32m in 0.53s\u001b[0m\u001b[0m\n" + ] + } + ], + "source": [ + "%%ipytest -s\n", + "\n", + "#| code-fold: true\n", + "#| output: false\n", + "\n", + "import os\n", + "import shutil\n", + "import tarfile\n", + "import pytest\n", + "import tempfile\n", + "import joblib\n", + "\n", + "from preprocessor import preprocess\n", + "from train import train\n", + "\n", + "\n", + "@pytest.fixture(scope=\"function\", autouse=False)\n", + "def directory():\n", + " directory = tempfile.mkdtemp()\n", + " input_directory = Path(directory) / \"input\"\n", + " input_directory.mkdir(parents=True, exist_ok=True)\n", + " shutil.copy2(DATA_FILEPATH, input_directory / \"data.csv\")\n", + " \n", + " directory = Path(directory)\n", + " \n", + " preprocess(base_directory=directory)\n", + " train(\n", + " model_directory=directory / \"model\",\n", + " train_path=directory / \"train\", \n", + " validation_path=directory / \"validation\",\n", + " epochs=1\n", + " )\n", + " \n", + " yield directory\n", + " \n", + " shutil.rmtree(directory)\n", + "\n", + "\n", + "def test_train_saves_a_folder_with_model_assets(directory):\n", + " output = os.listdir(directory / \"model\")\n", + " assert \"001\" in output\n", + " \n", + " assets = os.listdir(directory / \"model\" / \"001\")\n", + " assert \"saved_model.pb\" in assets" + ] + }, + { + "cell_type": "markdown", + "id": "27cff4c1-6510-4d99-8ae1-cb14927b87c7", + "metadata": {}, + "source": [ + "### Step 2 - Setting up the Training Step\n", + "\n", + "We can now create a [Training Step](https://docs.aws.amazon.com/sagemaker/latest/dg/build-and-manage-steps.html#step-type-training) that we can add to the pipeline. This Training Step will create a SageMaker Training Job in the background, run the training script, and upload the output to S3. Check the [TrainingStep](https://sagemaker.readthedocs.io/en/stable/workflows/pipelines/sagemaker.workflow.pipelines.html#sagemaker.workflow.steps.TrainingStep) SageMaker's SDK documentation for more information.\n", + "\n", + "SageMaker uses the concept of an [Estimator](https://sagemaker.readthedocs.io/en/stable/api/training/estimators.html) to handle end-to-end training and deployment tasks. For this example, we will use the built-in [TensorFlow Estimator](https://sagemaker.readthedocs.io/en/stable/frameworks/tensorflow/sagemaker.tensorflow.html#tensorflow-estimator) to run the training script we wrote before. The [Docker Registry Paths and Example Code](https://docs.aws.amazon.com/sagemaker/latest/dg/sagemaker-algo-docker-registry-paths.html) page contains information about the available framework versions for each region. Here, you can also check the available SageMaker [Deep Learning Container images](https://github.com/aws/deep-learning-containers/blob/master/available_images.md).\n", + "\n", + "Notice the list of hyperparameters defined below. SageMaker will pass these hyperparameters as arguments to the entry point of the training script.\n", + "\n", + "We are going to use [SageMaker Experiments](https://sagemaker.readthedocs.io/en/v2.174.0/experiments/sagemaker.experiments.html) to log information from the Training Job. For more information, check [Manage Machine Learning with Amazon SageMaker Experiments](https://docs.aws.amazon.com/sagemaker/latest/dg/experiments.html). The list of metric definitions will tell SageMaker which metrics to track and how to parse them from the Training Job logs.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 668, + "id": "90fe82ae-6a2c-4461-bc83-bb52d8871e3b", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "from sagemaker.tensorflow import TensorFlow\n", + "\n", + "estimator = TensorFlow(\n", + " base_job_name=\"training\",\n", + " entry_point=f\"{CODE_FOLDER}/train.py\",\n", + " # SageMaker will pass these hyperparameters as arguments\n", + " # to the entry point of the training script.\n", + " hyperparameters={\n", + " \"epochs\": 50,\n", + " \"batch_size\": 32,\n", + " },\n", + " # SageMaker will track these metrics as part of the experiment\n", + " # associated to this pipeline. The metric definitions tells\n", + " # SageMaker how to parse the values from the Training Job logs.\n", + " metric_definitions=[\n", + " {\"Name\": \"loss\", \"Regex\": \"loss: ([0-9\\\\.]+)\"},\n", + " {\"Name\": \"accuracy\", \"Regex\": \"accuracy: ([0-9\\\\.]+)\"},\n", + " {\"Name\": \"val_loss\", \"Regex\": \"val_loss: ([0-9\\\\.]+)\"},\n", + " {\"Name\": \"val_accuracy\", \"Regex\": \"val_accuracy: ([0-9\\\\.]+)\"},\n", + " ],\n", + " image_uri=config[\"image\"],\n", + " framework_version=config[\"framework_version\"],\n", + " py_version=config[\"py_version\"],\n", + " instance_type=config[\"instance_type\"],\n", + " instance_count=1,\n", + " disable_profiler=True,\n", + " sagemaker_session=config[\"session\"],\n", + " role=role,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "545d2b43-3bb5-4fe9-b3e4-cb8eb55c8a21", + "metadata": {}, + "source": [ + "We can now create a [Training Step](https://docs.aws.amazon.com/sagemaker/latest/dg/build-and-manage-steps.html#step-type-training). This Training Step will create a SageMaker Training Job in the background, run the training script, and upload the output to S3. Check the [TrainingStep](https://sagemaker.readthedocs.io/en/stable/workflows/pipelines/sagemaker.workflow.pipelines.html#sagemaker.workflow.steps.TrainingStep) SageMaker's SDK documentation for more information.\n", + "\n", + "This step will receive the train and validation split from the previous step as inputs.\n", + "\n", + "Here, we are using two input channels, `train` and `validation`. SageMaker will automatically create an environment variable corresponding to each of these channels following the format `SM_CHANNEL_[channel_name]`:\n", + "\n", + "- `SM_CHANNEL_TRAIN`: This environment variable will contain the path to the data in the `train` channel\n", + "- `SM_CHANNEL_VALIDATION`: This environment variable will contain the path to the data in the `validation` channel\n" + ] + }, + { + "cell_type": "code", + "execution_count": 738, + "id": "99e4850c-83d6-4f4e-a813-d5a3f4bb7486", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# | code: true\n", + "# | output: false\n", + "\n", + "from sagemaker.workflow.steps import TrainingStep\n", + "from sagemaker.inputs import TrainingInput\n", + "\n", + "train_model_step = TrainingStep(\n", + " name=\"train-model\",\n", + " step_args=estimator.fit(\n", + " inputs={\n", + " \"train\": TrainingInput(\n", + " s3_data=split_and_transform_data_step.properties.ProcessingOutputConfig.Outputs[\n", + " \"train\"\n", + " ].S3Output.S3Uri,\n", + " content_type=\"text/csv\",\n", + " ),\n", + " \"validation\": TrainingInput(\n", + " s3_data=split_and_transform_data_step.properties.ProcessingOutputConfig.Outputs[\n", + " \"validation\"\n", + " ].S3Output.S3Uri,\n", + " content_type=\"text/csv\",\n", + " ),\n", + " }\n", + " ),\n", + " cache_config=cache_config,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "5814e258-c633-4e9a-85c5-6ed0f168b503", + "metadata": {}, + "source": [ + "### Step 3 - Setting up a Tuning Step\n", + "\n", + "Let's now create a [Tuning Step](https://docs.aws.amazon.com/sagemaker/latest/dg/build-and-manage-steps.html#step-type-tuning). This Tuning Step will create a SageMaker Hyperparameter Tuning Job in the background and use the training script to train different model variants and choose the best one. Check the [TuningStep](https://sagemaker.readthedocs.io/en/stable/workflows/pipelines/sagemaker.workflow.pipelines.html#sagemaker.workflow.steps.TuningStep) SageMaker's SDK documentation for more information.\n" + ] + }, + { + "cell_type": "markdown", + "id": "90eb5075", + "metadata": {}, + "source": [ + "Since we could use the Training of the Tuning Step to create the model, we'll define this constant to indicate which approach we want to run.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 670, + "id": "f367d0e3", + "metadata": {}, + "outputs": [], + "source": [ + "USE_TUNING_STEP = False" + ] + }, + { + "cell_type": "markdown", + "id": "b045af84", + "metadata": {}, + "source": [ + "The Tuning Step requires a [HyperparameterTuner](https://sagemaker.readthedocs.io/en/stable/api/training/tuner.html) reference to configure the Hyperparameter Tuning Job.\n", + "\n", + "Here is the configuration that we'll use to find the best model:\n", + "\n", + "1. `objective_metric_name`: This is the name of the metric the tuner will use to determine the best model.\n", + "2. `objective_type`: This is the objective of the tuner. Should it \"Minimize\" the metric or \"Maximize\" it? In this example, since we are using the validation accuracy of the model, we want the objective to be \"Maximize.\" If we were using the loss of the model, we would set the objective to \"Minimize.\"\n", + "3. `metric_definitions`: Defines how the tuner will determine the metric's value by looking at the output logs of the training process.\n", + "\n", + "The tuner expects the list of the hyperparameters you want to explore. You can use subclasses of the [Parameter](https://sagemaker.readthedocs.io/en/stable/api/training/parameter.html#sagemaker.parameter.ParameterRange) class to specify different types of hyperparameters. This example explores different values for the `epochs` hyperparameter.\n", + "\n", + "Finally, you can control the number of jobs and how many of them will run in parallel using the following two arguments:\n", + "\n", + "- `max_jobs`: Defines the maximum total number of training jobs to start for the hyperparameter tuning job.\n", + "- `max_parallel_jobs`: Defines the maximum number of parallel training jobs to start.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 671, + "id": "c8c82750", + "metadata": {}, + "outputs": [], + "source": [ + "from sagemaker.tuner import HyperparameterTuner\n", + "from sagemaker.parameter import IntegerParameter\n", + "\n", + "tuner = HyperparameterTuner(\n", + " estimator,\n", + " objective_metric_name=\"val_accuracy\",\n", + " objective_type=\"Maximize\",\n", + " hyperparameter_ranges={\n", + " \"epochs\": IntegerParameter(10, 50),\n", + " },\n", + " metric_definitions=[{\"Name\": \"val_accuracy\", \"Regex\": \"val_accuracy: ([0-9\\\\.]+)\"}],\n", + " max_jobs=3,\n", + " max_parallel_jobs=3,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "28c2abc2", + "metadata": {}, + "source": [ + "We can now create the Tuning Step using the tuner we configured before:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 672, + "id": "038ff2e5-ed28-445b-bc03-4e996ec2286f", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "from sagemaker.workflow.steps import TuningStep\n", + "\n", + "tune_model_step = TuningStep(\n", + " name=\"tune-model\",\n", + " step_args=tuner.fit(\n", + " inputs={\n", + " \"train\": TrainingInput(\n", + " s3_data=split_and_transform_data_step.properties.ProcessingOutputConfig.Outputs[\n", + " \"train\"\n", + " ].S3Output.S3Uri,\n", + " content_type=\"text/csv\",\n", + " ),\n", + " \"validation\": TrainingInput(\n", + " s3_data=split_and_transform_data_step.properties.ProcessingOutputConfig.Outputs[\n", + " \"validation\"\n", + " ].S3Output.S3Uri,\n", + " content_type=\"text/csv\",\n", + " ),\n", + " },\n", + " ),\n", + " cache_config=cache_config,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "4babe38c-1682-42d2-8442-101d17aa89b5", + "metadata": {}, + "source": [ + "### Step 4 - Creating the Pipeline\n", + "\n", + "Let's define the SageMaker Pipeline and submit its definition to the SageMaker Pipelines service to create the pipeline if it doesn't exist or update it if it does.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 673, + "id": "9799ab39-fcae-41f4-a68b-85ab71b3ba9a", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO:sagemaker.image_uris:image_uri is not presented, retrieving image_uri based on instance_type, framework etc.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Using provided s3_resource\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO:sagemaker.image_uris:image_uri is not presented, retrieving image_uri based on instance_type, framework etc.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Using provided s3_resource\n" + ] + }, + { + "data": { + "text/plain": [ + "{'PipelineArn': 'arn:aws:sagemaker:us-east-1:325223348818:pipeline/session2-pipeline',\n", + " 'ResponseMetadata': {'RequestId': 'e99208aa-4074-41aa-a12b-90af6da62e3f',\n", + " 'HTTPStatusCode': 200,\n", + " 'HTTPHeaders': {'x-amzn-requestid': 'e99208aa-4074-41aa-a12b-90af6da62e3f',\n", + " 'content-type': 'application/x-amz-json-1.1',\n", + " 'content-length': '85',\n", + " 'date': 'Fri, 27 Oct 2023 14:38:38 GMT'},\n", + " 'RetryAttempts': 0}}" + ] + }, + "execution_count": 673, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# | code: true\n", + "# | output: false\n", + "\n", + "session2_pipeline = Pipeline(\n", + " name=\"session2-pipeline\",\n", + " parameters=[dataset_location],\n", + " steps=[\n", + " split_and_transform_data_step,\n", + " tune_model_step if USE_TUNING_STEP else train_model_step,\n", + " ],\n", + " pipeline_definition_config=pipeline_definition_config,\n", + " sagemaker_session=config[\"session\"],\n", + ")\n", + "\n", + "session2_pipeline.upsert(role_arn=role)" + ] + }, + { + "cell_type": "markdown", + "id": "50810a3e", + "metadata": {}, + "source": [ + "We can now start the pipeline:\n" + ] + }, + { + "cell_type": "markdown", + "id": "6bcb9d05", + "metadata": {}, + "source": [ + "#| hide\n", + "\n", + "
Note: \n", + " The %%script cell magic is a convenient way to prevent the notebook from executing a specific cell. If you want to run the cell, comment out the line containing the %%script cell magic.\n", + "
\n" + ] + }, + { + "cell_type": "code", + "execution_count": 674, + "id": "274a9b1e", + "metadata": {}, + "outputs": [], + "source": [ + "%%script false --no-raise-error\n", + "\n", + "#| eval: false\n", + "#| code: true\n", + "#| output: false\n", + "\n", + "session2_pipeline.start()" + ] + }, + { + "cell_type": "markdown", + "id": "c044516e-f56c-4c91-8d94-6ef109eb7325", + "metadata": {}, + "source": [ + "### Assignments\n", + "\n", + "- Assignment 2.1 The training script trains the model using a hard-coded learning rate value. Modify the code to accept the learning rate as a parameter we can control from outside the script.\n", + "\n", + "- Assignment 2.2 We currently define the number of epochs to train the model as a constant that we pass to the Estimator using the list of hyperparameters. Replace this constant with a new Pipeline Parameter named `training_epochs`. You'll need to specify this new parameter when creating the Pipeline.\n", + "\n", + "- Assignment 2.3 The current tuning process aims to find the model with the highest validation accuracy. Modify the code to focus on the model with the lowest training loss.\n", + "\n", + "- Assignment 2.4 We used an instance of [`SKLearnProcessor`](https://sagemaker.readthedocs.io/en/stable/frameworks/sklearn/sagemaker.sklearn.html#scikit-learn-processor) to run the script that transforms and splits the data, but there's no way to add additional dependencies to the processing container. Modify the code to use an instance of [`FrameworkProcessor`](https://sagemaker.readthedocs.io/en/stable/api/training/processing.html#sagemaker.processing.FrameworkProcessor) instead. This class will allow you to specify a directory containing a `requirements.txt` file containing a list of dependencies. SageMaker will install these libraries in the processing container before triggering the processing job.\n", + "\n", + "- Assignment 2.5 We want to execute the pipeline whenever the dataset changes. We can accomplish this by using [Amazon EventBridge](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-what-is.html). Configure an event to automatically start the pipeline when a new file is added to the S3 bucket where we store our dataset. Check [Amazon EventBridge Integration](https://docs.aws.amazon.com/sagemaker/latest/dg/pipeline-eventbridge.html) for an implementation tutorial.\n" + ] + }, + { + "cell_type": "markdown", + "id": "21d40fe8-ba74-4c12-9555-d8ea33d1c8b4", + "metadata": {}, + "source": [ + "## Session 3 - Evaluating and Versioning Models\n", + "\n", + "This session extends the [SageMaker Pipeline](https://docs.aws.amazon.com/sagemaker/latest/dg/pipelines-sdk.html) with a step to evaluate the model and register it if it reaches a predefined accuracy threshold.\n", + "\n", + " \"Training\"\n", + "\n", + "We'll use a [Processing Step](https://docs.aws.amazon.com/sagemaker/latest/dg/build-and-manage-steps.html#step-type-processing) to execute an evaluation script. We'll use a [Condition Step](https://docs.aws.amazon.com/sagemaker/latest/dg/build-and-manage-steps.html#step-type-condition) to determine whether the model's accuracy is above a threshold, and a [Model Step](https://docs.aws.amazon.com/sagemaker/latest/dg/build-and-manage-steps.html#step-type-model) to register the model in the [SageMaker Model Registry](https://docs.aws.amazon.com/sagemaker/latest/dg/model-registry.html).\n" + ] + }, + { + "cell_type": "markdown", + "id": "9eaa9691-f49f-48af-b272-3d4d17563b01", + "metadata": { + "tags": [] + }, + "source": [ + "### Step 1 - Creating the Evaluation Script\n", + "\n", + "Let's create the evaluation script. The Processing Step will spin up a Processing Job and run this script inside a container. This script is responsible for loading the model we created and evaluating it on the test set. Before finishing, this script will generate an evaluation report of the model.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 675, + "id": "3ee3ab26-afa5-4ceb-9f7a-005d5fdea646", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Overwriting code/evaluation.py\n" + ] + } + ], + "source": [ + "%%writefile {CODE_FOLDER}/evaluation.py\n", + "#| label: evaluation-script\n", + "#| echo: true\n", + "#| output: false\n", + "#| filename: evaluation.py\n", + "#| code-line-numbers: true\n", + "\n", + "import os\n", + "import json\n", + "import tarfile\n", + "import numpy as np\n", + "import pandas as pd\n", + "\n", + "from pathlib import Path\n", + "from tensorflow import keras\n", + "from sklearn.metrics import accuracy_score\n", + "\n", + "\n", + "MODEL_PATH = \"/opt/ml/processing/model/\"\n", + "TEST_PATH = \"/opt/ml/processing/test/\"\n", + "OUTPUT_PATH = \"/opt/ml/processing/evaluation/\"\n", + "\n", + "\n", + "def evaluate(model_path, test_path, output_path):\n", + " # The first step is to extract the model package so we can load \n", + " # it in memory.\n", + " with tarfile.open(Path(model_path) / \"model.tar.gz\") as tar:\n", + " tar.extractall(path=Path(model_path))\n", + " \n", + " model = keras.models.load_model(Path(model_path) / \"001\")\n", + " \n", + " X_test = pd.read_csv(Path(test_path) / \"test.csv\")\n", + " y_test = X_test[X_test.columns[-1]]\n", + " X_test.drop(X_test.columns[-1], axis=1, inplace=True)\n", + " \n", + " predictions = np.argmax(model.predict(X_test), axis=-1)\n", + " accuracy = accuracy_score(y_test, predictions)\n", + " print(f\"Test accuracy: {accuracy}\")\n", + "\n", + " # Let's create an evaluation report using the model accuracy.\n", + " evaluation_report = {\n", + " \"metrics\": {\n", + " \"accuracy\": {\n", + " \"value\": accuracy\n", + " },\n", + " },\n", + " }\n", + " \n", + " Path(output_path).mkdir(parents=True, exist_ok=True)\n", + " with open(Path(output_path) / \"evaluation.json\", \"w\") as f:\n", + " f.write(json.dumps(evaluation_report))\n", + " \n", + " \n", + "if __name__ == \"__main__\":\n", + " evaluate(\n", + " model_path=MODEL_PATH, \n", + " test_path=TEST_PATH,\n", + " output_path=OUTPUT_PATH\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "9dcc79a0-adfd-4ce9-8580-5cd228c3c2d9", + "metadata": {}, + "source": [ + "Let's test the script to ensure everything is working as expected:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 676, + "id": "9a2540d8-278a-4953-bc54-0469d154427d", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "WARNING:absl:At this time, the v2.11+ optimizer `tf.keras.optimizers.SGD` runs slowly on M1/M2 Macs, please use the legacy Keras optimizer instead, located at `tf.keras.optimizers.legacy.SGD`.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "8/8 - 0s - loss: 1.1330 - accuracy: 0.4142 - val_loss: 1.1001 - val_accuracy: 0.5098 - 236ms/epoch - 30ms/step\n", + "2/2 [==============================] - 0s 1ms/step\n", + "Validation accuracy: 0.5098039215686274\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO:tensorflow:Assets written to: /var/folders/4c/v1q3hy1x4mb5w0wpc72zl3_w0000gp/T/tmpprbc5h18/model/001/assets\n", + "WARNING:absl:At this time, the v2.11+ optimizer `tf.keras.optimizers.RestoredOptimizer` runs slowly on M1/M2 Macs, please use the legacy Keras optimizer instead, located at `tf.keras.optimizers.legacy.RestoredOptimizer`.\n", + "WARNING:absl:At this time, the v2.11+ optimizer `tf.keras.optimizers.SGD` runs slowly on M1/M2 Macs, please use the legacy Keras optimizer instead, located at `tf.keras.optimizers.legacy.SGD`.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2/2 [==============================] - 0s 1ms/step\n", + "Test accuracy: 0.4117647058823529\n", + "\u001b[32m.\u001b[0m" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "WARNING:absl:At this time, the v2.11+ optimizer `tf.keras.optimizers.SGD` runs slowly on M1/M2 Macs, please use the legacy Keras optimizer instead, located at `tf.keras.optimizers.legacy.SGD`.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "8/8 - 0s - loss: 1.0329 - accuracy: 0.4644 - val_loss: 0.9795 - val_accuracy: 0.5882 - 235ms/epoch - 29ms/step\n", + "2/2 [==============================] - 0s 1ms/step\n", + "Validation accuracy: 0.5882352941176471\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO:tensorflow:Assets written to: /var/folders/4c/v1q3hy1x4mb5w0wpc72zl3_w0000gp/T/tmph0nj0wfb/model/001/assets\n", + "WARNING:absl:At this time, the v2.11+ optimizer `tf.keras.optimizers.RestoredOptimizer` runs slowly on M1/M2 Macs, please use the legacy Keras optimizer instead, located at `tf.keras.optimizers.legacy.RestoredOptimizer`.\n", + "WARNING:absl:At this time, the v2.11+ optimizer `tf.keras.optimizers.SGD` runs slowly on M1/M2 Macs, please use the legacy Keras optimizer instead, located at `tf.keras.optimizers.legacy.SGD`.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2/2 [==============================] - 0s 2ms/step\n", + "Test accuracy: 0.5686274509803921\n", + "\u001b[32m.\u001b[0m\n", + "\u001b[32m\u001b[32m\u001b[1m2 passed\u001b[0m\u001b[32m in 1.35s\u001b[0m\u001b[0m\n" + ] + } + ], + "source": [ + "%%ipytest -s\n", + "\n", + "#| code-fold: true\n", + "#| output: false\n", + "\n", + "import os\n", + "import shutil\n", + "import tarfile\n", + "import pytest\n", + "import tempfile\n", + "import joblib\n", + "\n", + "from preprocessor import preprocess\n", + "from train import train\n", + "from evaluation import evaluate\n", + "\n", + "\n", + "@pytest.fixture(scope=\"function\", autouse=False)\n", + "def directory():\n", + " directory = tempfile.mkdtemp()\n", + " input_directory = Path(directory) / \"input\"\n", + " input_directory.mkdir(parents=True, exist_ok=True)\n", + " shutil.copy2(DATA_FILEPATH, input_directory / \"data.csv\")\n", + " \n", + " directory = Path(directory)\n", + " \n", + " preprocess(base_directory=directory)\n", + " \n", + " train(\n", + " model_directory=directory / \"model\",\n", + " train_path=directory / \"train\", \n", + " validation_path=directory / \"validation\",\n", + " epochs=1\n", + " )\n", + " \n", + " # After training a model, we need to prepare a package just like\n", + " # SageMaker would. This package is what the evaluation script is\n", + " # expecting as an input.\n", + " with tarfile.open(directory / \"model.tar.gz\", \"w:gz\") as tar:\n", + " tar.add(directory / \"model\" / \"001\", arcname=\"001\")\n", + " \n", + " evaluate(\n", + " model_path=directory, \n", + " test_path=directory / \"test\",\n", + " output_path=directory / \"evaluation\",\n", + " )\n", + "\n", + " yield directory / \"evaluation\"\n", + " \n", + " shutil.rmtree(directory)\n", + "\n", + "\n", + "def test_evaluate_generates_evaluation_report(directory):\n", + " output = os.listdir(directory)\n", + " assert \"evaluation.json\" in output\n", + "\n", + "\n", + "def test_evaluation_report_contains_accuracy(directory):\n", + " with open(directory / \"evaluation.json\", 'r') as file:\n", + " report = json.load(file)\n", + " \n", + " assert \"metrics\" in report\n", + " assert \"accuracy\" in report[\"metrics\"]\n", + " " + ] + }, + { + "cell_type": "markdown", + "id": "bec1109a-6c26-4464-8338-94960729d212", + "metadata": {}, + "source": [ + "### Step 2 - Setting up the Evaluation Step\n", + "\n", + "To run the evaluation script, we will use a [Processing Step](https://docs.aws.amazon.com/sagemaker/latest/dg/build-and-manage-steps.html#step-type-processing) configured with [TensorFlowProcessor](https://docs.aws.amazon.com/sagemaker/latest/dg/processing-job-frameworks-tensorflow.html) because the script needs access to TensorFlow.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 677, + "id": "2fdff07f", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO:sagemaker.image_uris:image_uri is not presented, retrieving image_uri based on instance_type, framework etc.\n" + ] + } + ], + "source": [ + "# | code: true\n", + "# | output: false\n", + "\n", + "from sagemaker.tensorflow import TensorFlowProcessor\n", + "\n", + "tensorflow_processor = TensorFlowProcessor(\n", + " base_job_name=\"evaluation-processor\",\n", + " image_uri=config[\"image\"],\n", + " framework_version=config[\"framework_version\"],\n", + " py_version=config[\"py_version\"],\n", + " instance_type=config[\"instance_type\"],\n", + " instance_count=1,\n", + " role=role,\n", + " sagemaker_session=config[\"session\"],\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "419e354a", + "metadata": {}, + "source": [ + "One of the inputs to the Evaluation Step will be the model assets. We can use the `USE_TUNING_STEP` flag to determine whether we created the model using a Training Step or a Tuning Step. In case we are using the Tuning Step, we can use the [TuningStep.get_top_model_s3_uri()](https://sagemaker.readthedocs.io/en/stable/workflows/pipelines/sagemaker.workflow.pipelines.html#sagemaker.workflow.steps.TuningStep.get_top_model_s3_uri) function to get the model assets from the top performing training job of the Hyperparameter Tuning Job.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 678, + "id": "4f19e15b", + "metadata": {}, + "outputs": [], + "source": [ + "model_assets = train_model_step.properties.ModelArtifacts.S3ModelArtifacts\n", + "\n", + "if USE_TUNING_STEP:\n", + " model_assets = tune_model_step.get_top_model_s3_uri(\n", + " top_k=0, s3_bucket=config[\"session\"].default_bucket()\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "08dae772", + "metadata": {}, + "source": [ + "SageMaker supports mapping outputs to property files. This is useful when accessing a specific property from the pipeline. In our case, we want to access the accuracy of the model in the Condition Step, so we'll map the evaluation report to a property file. Check [How to Build and Manage Property Files](https://docs.aws.amazon.com/sagemaker/latest/dg/build-and-manage-propertyfile.html) for more information.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 679, + "id": "1f27b2ef", + "metadata": {}, + "outputs": [], + "source": [ + "from sagemaker.workflow.properties import PropertyFile\n", + "\n", + "evaluation_report = PropertyFile(\n", + " name=\"evaluation-report\", output_name=\"evaluation\", path=\"evaluation.json\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "4a4dbc0e", + "metadata": {}, + "source": [ + "We are now ready to define the [ProcessingStep](https://sagemaker.readthedocs.io/en/stable/workflows/pipelines/sagemaker.workflow.pipelines.html#sagemaker.workflow.steps.ProcessingStep) that will run the evaluation script:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 680, + "id": "48139a07-5c8e-4bc6-b666-bf9531f7f520", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/svpino/dev/ml.school/.venv/lib/python3.9/site-packages/sagemaker/workflow/pipeline_context.py:297: UserWarning: Running within a PipelineSession, there will be No Wait, No Logs, and No Job being started.\n", + " warnings.warn(\n" + ] + } + ], + "source": [ + "# | code: true\n", + "# | output: false\n", + "\n", + "evaluate_model_step = ProcessingStep(\n", + " name=\"evaluate-model\",\n", + " step_args=tensorflow_processor.run(\n", + " inputs=[\n", + " # The first input is the test split that we generated on\n", + " # the first step of the pipeline when we split and\n", + " # transformed the data.\n", + " ProcessingInput(\n", + " source=split_and_transform_data_step.properties.ProcessingOutputConfig.Outputs[\n", + " \"test\"\n", + " ].S3Output.S3Uri,\n", + " destination=\"/opt/ml/processing/test\",\n", + " ),\n", + " # The second input is the model that we generated on\n", + " # the Training or Tunning Step.\n", + " ProcessingInput(\n", + " source=model_assets,\n", + " destination=\"/opt/ml/processing/model\",\n", + " ),\n", + " ],\n", + " outputs=[\n", + " # The output is the evaluation report that we generated\n", + " # in the evaluation script.\n", + " ProcessingOutput(\n", + " output_name=\"evaluation\", source=\"/opt/ml/processing/evaluation\"\n", + " ),\n", + " ],\n", + " code=f\"{CODE_FOLDER}/evaluation.py\",\n", + " ),\n", + " property_files=[evaluation_report],\n", + " cache_config=cache_config,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "328a2bf2", + "metadata": {}, + "source": [ + "### Step 3 - Registering the Model\n", + "\n", + "Let's now create a new version of the model and register it in the Model Registry. Check [Register a Model Version](https://docs.aws.amazon.com/sagemaker/latest/dg/model-registry-version.html) for more information about model registration.\n", + "\n", + "First, let's define the name of the group where we'll register the model:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 681, + "id": "bb70f907", + "metadata": {}, + "outputs": [], + "source": [ + "MODEL_PACKAGE_GROUP = \"penguins\"" + ] + }, + { + "cell_type": "markdown", + "id": "40bcad3b", + "metadata": {}, + "source": [ + "Let's now create the model that we'll register in the Model Registry. The model we trained uses TensorFlow, so we can use the built-in [TensorFlowModel](https://sagemaker.readthedocs.io/en/stable/frameworks/tensorflow/sagemaker.tensorflow.html#tensorflow-serving-model) class to create an instance of the model:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 682, + "id": "4ca4cb61", + "metadata": {}, + "outputs": [], + "source": [ + "from sagemaker.tensorflow.model import TensorFlowModel\n", + "\n", + "tensorflow_model = TensorFlowModel(\n", + " model_data=model_assets,\n", + " image_uri=config[\"image\"],\n", + " framework_version=config[\"framework_version\"],\n", + " sagemaker_session=config[\"session\"],\n", + " role=role,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "99d6fd00", + "metadata": {}, + "source": [ + "When we register a model in the Model Registry, we can attach relevant metadata to it. We'll use the evaluation report we generated during the Evaluation Step to populate the [metrics](https://sagemaker.readthedocs.io/en/stable/api/inference/model_monitor.html#sagemaker.model_metrics.ModelMetrics) of this model:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 683, + "id": "8c05a7e1", + "metadata": {}, + "outputs": [], + "source": [ + "from sagemaker.model_metrics import ModelMetrics, MetricsSource\n", + "from sagemaker.workflow.functions import Join\n", + "\n", + "model_metrics = ModelMetrics(\n", + " model_statistics=MetricsSource(\n", + " s3_uri=Join(\n", + " on=\"/\",\n", + " values=[\n", + " evaluate_model_step.properties.ProcessingOutputConfig.Outputs[\n", + " \"evaluation\"\n", + " ].S3Output.S3Uri,\n", + " \"evaluation.json\",\n", + " ],\n", + " ),\n", + " content_type=\"application/json\",\n", + " )\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "6a51e61d", + "metadata": {}, + "source": [ + "We can use a [Model Step](https://docs.aws.amazon.com/sagemaker/latest/dg/build-and-manage-steps.html#step-type-model) to register the model. Check the [ModelStep](https://sagemaker.readthedocs.io/en/stable/workflows/pipelines/sagemaker.workflow.pipelines.html#sagemaker.workflow.model_step.ModelStep) SageMaker's SDK documentation for more information.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 684, + "id": "c9773a4a", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO:sagemaker.tensorflow.model:image_uri is not presented, retrieving image_uri based on instance_type, framework etc.\n" + ] + } + ], + "source": [ + "# | code: true\n", + "# | output: false\n", + "\n", + "from sagemaker.workflow.model_step import ModelStep\n", + "\n", + "register_model_step = ModelStep(\n", + " name=\"register-model\",\n", + " step_args=tensorflow_model.register(\n", + " model_package_group_name=MODEL_PACKAGE_GROUP,\n", + " approval_status=\"Approved\",\n", + " model_metrics=model_metrics,\n", + " content_types=[\"text/csv\"],\n", + " response_types=[\"text/csv\"],\n", + " # This is the suggested inference instance types when\n", + " # deploying the model or using it as part of a batch\n", + " # transform job.\n", + " inference_instances=[\"ml.m5.xlarge\"],\n", + " transform_instances=[\"ml.m5.xlarge\"],\n", + " domain=\"MACHINE_LEARNING\",\n", + " task=\"CLASSIFICATION\",\n", + " framework=\"TENSORFLOW\",\n", + " framework_version=config[\"framework_version\"],\n", + " ),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "52c110f7-fe72-4db8-9d06-cfb9a0f2bfbd", + "metadata": {}, + "source": [ + "### Step 4 - Setting up a Condition Step\n", + "\n", + "We only want to register a new model if its accuracy exceeds a predefined threshold. We can use a [Condition Step](https://docs.aws.amazon.com/sagemaker/latest/dg/build-and-manage-steps.html#step-type-condition) together with the evaluation report we generated to accomplish this.\n" + ] + }, + { + "cell_type": "markdown", + "id": "b5a51f95", + "metadata": {}, + "source": [ + "Let's define a new [Pipeline Parameter](https://docs.aws.amazon.com/sagemaker/latest/dg/build-and-manage-parameters.html) to specify the minimum accuracy that the model should reach for it to be registered.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 685, + "id": "745486b5", + "metadata": {}, + "outputs": [], + "source": [ + "from sagemaker.workflow.parameters import ParameterFloat\n", + "\n", + "accuracy_threshold = ParameterFloat(name=\"accuracy_threshold\", default_value=0.70)" + ] + }, + { + "cell_type": "markdown", + "id": "2c959c94", + "metadata": {}, + "source": [ + "If the model's accuracy is not greater than or equal our threshold, we will send the pipeline to a [Fail Step](https://docs.aws.amazon.com/sagemaker/latest/dg/build-and-manage-steps.html#step-type-fail) with the appropriate error message. Check the [FailStep](https://sagemaker.readthedocs.io/en/stable/workflows/pipelines/sagemaker.workflow.pipelines.html#sagemaker.workflow.fail_step.FailStep) SageMaker's SDK documentation for more information.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 686, + "id": "c4431bbf", + "metadata": {}, + "outputs": [], + "source": [ + "from sagemaker.workflow.fail_step import FailStep\n", + "\n", + "fail_step = FailStep(\n", + " name=\"fail\",\n", + " error_message=Join(\n", + " on=\" \",\n", + " values=[\n", + " \"Execution failed because the model's accuracy was lower than\",\n", + " accuracy_threshold,\n", + " ],\n", + " ),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "b47764f9", + "metadata": {}, + "source": [ + "We can use a [ConditionGreaterThanOrEqualTo](https://sagemaker.readthedocs.io/en/stable/workflows/pipelines/sagemaker.workflow.pipelines.html#sagemaker.workflow.conditions.ConditionGreaterThanOrEqualTo) condition to compare the model's accuracy with the threshold. Look at the [Conditions](https://sagemaker.readthedocs.io/en/stable/amazon_sagemaker_model_building_pipeline.html#conditions) section in the documentation for more information about the types of supported conditions.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 687, + "id": "bebeecab", + "metadata": {}, + "outputs": [], + "source": [ + "from sagemaker.workflow.functions import JsonGet\n", + "from sagemaker.workflow.conditions import ConditionGreaterThanOrEqualTo\n", + "\n", + "condition = ConditionGreaterThanOrEqualTo(\n", + " left=JsonGet(\n", + " step_name=evaluate_model_step.name,\n", + " property_file=evaluation_report,\n", + " json_path=\"metrics.accuracy.value\",\n", + " ),\n", + " right=accuracy_threshold,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "1b0ce4b1", + "metadata": {}, + "source": [ + "Let's now define the Condition Step:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 688, + "id": "36e2a2b1-6711-4266-95d8-d2aebd52e199", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "from sagemaker.workflow.condition_step import ConditionStep\n", + "\n", + "condition_step = ConditionStep(\n", + " name=\"check-model-accuracy\",\n", + " conditions=[condition],\n", + " if_steps=[register_model_step] if not LOCAL_MODE else [],\n", + " else_steps=[fail_step],\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "2309b8fa-f03e-4959-853f-dc2416f82bdd", + "metadata": {}, + "source": [ + "### Step 5 - Creating the Pipeline\n", + "\n", + "We can now define the SageMaker Pipeline and submit its definition to the SageMaker Pipelines service to create the pipeline if it doesn't exist or update it if it does.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 689, + "id": "f70bcd33-b499-4e2b-953e-94d1ed96c10a", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO:sagemaker.image_uris:image_uri is not presented, retrieving image_uri based on instance_type, framework etc.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Using provided s3_resource\n", + "Using provided s3_resource\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO:sagemaker.processing:Uploaded None to s3://mlschool/session3-pipeline/code/09fea667a5ab7c37a068f22c00762d0b/sourcedir.tar.gz\n", + "INFO:sagemaker.processing:runproc.sh uploaded to s3://mlschool/session3-pipeline/code/2c207c809cb0e0e9a1d77e5247f961f9/runproc.sh\n", + "WARNING:sagemaker.workflow._utils:Popping out 'CertifyForMarketplace' from the pipeline definition since it will be overridden in pipeline execution time.\n", + "INFO:sagemaker.image_uris:image_uri is not presented, retrieving image_uri based on instance_type, framework etc.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Using provided s3_resource\n", + "Using provided s3_resource\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO:sagemaker.processing:Uploaded None to s3://mlschool/session3-pipeline/code/09fea667a5ab7c37a068f22c00762d0b/sourcedir.tar.gz\n", + "INFO:sagemaker.processing:runproc.sh uploaded to s3://mlschool/session3-pipeline/code/2c207c809cb0e0e9a1d77e5247f961f9/runproc.sh\n" + ] + }, + { + "data": { + "text/plain": [ + "{'PipelineArn': 'arn:aws:sagemaker:us-east-1:325223348818:pipeline/session3-pipeline',\n", + " 'ResponseMetadata': {'RequestId': 'be91a772-a26a-4c1f-a98a-424951e6889a',\n", + " 'HTTPStatusCode': 200,\n", + " 'HTTPHeaders': {'x-amzn-requestid': 'be91a772-a26a-4c1f-a98a-424951e6889a',\n", + " 'content-type': 'application/x-amz-json-1.1',\n", + " 'content-length': '85',\n", + " 'date': 'Fri, 27 Oct 2023 14:38:43 GMT'},\n", + " 'RetryAttempts': 0}}" + ] + }, + "execution_count": 689, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# | code: true\n", + "# | output: false\n", + "\n", + "session3_pipeline = Pipeline(\n", + " name=\"session3-pipeline\",\n", + " parameters=[dataset_location, accuracy_threshold],\n", + " steps=[\n", + " split_and_transform_data_step,\n", + " tune_model_step if USE_TUNING_STEP else train_model_step,\n", + " evaluate_model_step,\n", + " condition_step,\n", + " ],\n", + " pipeline_definition_config=pipeline_definition_config,\n", + " sagemaker_session=config[\"session\"],\n", + ")\n", + "\n", + "session3_pipeline.upsert(role_arn=role)" + ] + }, + { + "cell_type": "markdown", + "id": "1b1f656e", + "metadata": {}, + "source": [ + "We can now start the pipeline:" + ] + }, + { + "cell_type": "markdown", + "id": "36144169", + "metadata": {}, + "source": [ + "#| hide\n", + "\n", + "
Note: \n", + " The %%script cell magic is a convenient way to prevent the notebook from executing a specific cell. If you want to run the cell, comment out the line containing the %%script cell magic.\n", + "
\n" + ] + }, + { + "cell_type": "code", + "execution_count": 690, + "id": "f3b4126e", + "metadata": {}, + "outputs": [], + "source": [ + "%%script false --no-raise-error\n", + "\n", + "#| eval: false\n", + "#| code: true\n", + "#| output: false\n", + "\n", + "session3_pipeline.start()" + ] + }, + { + "cell_type": "markdown", + "id": "9418693c-ccd5-42b6-8ec4-04bb70fe213c", + "metadata": {}, + "source": [ + "### Assignments\n", + "\n", + "- Assignment 3.1 The evaluation script computes the accuracy of the model and exports it as part of the evaluation report. Extend the evaluation report by adding the precision and the recall of the model on each one of the classes.\n", + "\n", + "- Assignment 3.2 Extend the evaluation script to test the model on each island separately. The evaluation report should contain the accuracy of the model on each island and the overall accuracy.\n", + "\n", + "- Assignment 3.3 The Condition Step uses a hard-coded threshold value to determine if the model's accuracy is good enough to proceed. Modify the code so the pipeline uses the accuracy of the latest registered model version as the threshold. We want to register a new model version only if its performance is better than the previous version we registered.\n", + "\n", + "- Assignment 3.4 The current pipeline uses either a Training Step or a Tuning Step to build a model. Modify the pipeline to use both steps at the same time. The evaluation script should evaluate the model coming from the Training Step and the best model coming from the Tuning Step and output the accuracy and location in S3 of the best model. You should modify the code to register the model assets specified in the evaluation report.\n", + "\n", + "- Assignment 3.5 Pipeline steps can encounter exceptions. In some cases, retrying can resolve these issues. For this assignment, configure the Processing Step so it automatically retries the step a maximum of 5 times if it encounters an `InternalServerError`. Check the [Retry Policy for Pipeline Steps](https://docs.aws.amazon.com/sagemaker/latest/dg/pipelines-retry-policy.html) documentation for more information." + ] + }, + { + "cell_type": "markdown", + "id": "565cf77e-7fc7-406e-a2e2-40c553f459f7", + "metadata": { + "tags": [] + }, + "source": [ + "## Session 4 - Deploying Models and Serving Predictions\n", + "\n", + "In this session we'll explore how to deploy a model to a SageMaker Endpoint and how to use a SageMaker Inference Pipeline to control the data that goes in and comes out of the endpoint.\n", + "\n", + " \"Deployment\"\n", + "\n", + "Let's start by defining the name of the endpoint where we'll deploy the model and creating a constant pointing to the location where we'll store the data that the endpoint will capture:" + ] + }, + { + "cell_type": "code", + "execution_count": 691, + "id": "befd5ad3", + "metadata": {}, + "outputs": [], + "source": [ + "from sagemaker.predictor import Predictor\n", + "\n", + "ENDPOINT = \"penguins-endpoint\"\n", + "DATA_CAPTURE_DESTINATION = f\"{S3_LOCATION}/monitoring/data-capture\"" + ] + }, + { + "cell_type": "markdown", + "id": "93727425-fac6-44ec-91ed-130a50fdd18a", + "metadata": {}, + "source": [ + "### Step 1 - Deploying Model From Registry\n", + "\n", + "Let's manually deploy the latest model from the Model Registry to an endpoint.\n", + "\n", + "We want to query the list of approved models from the Model Registry and get the last one:" + ] + }, + { + "cell_type": "code", + "execution_count": 692, + "id": "87437a26-e9ea-4866-9dc3-630444c0fb46", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{'ModelPackageGroupName': 'penguins',\n", + " 'ModelPackageVersion': 74,\n", + " 'ModelPackageArn': 'arn:aws:sagemaker:us-east-1:325223348818:model-package/penguins/74',\n", + " 'CreationTime': datetime.datetime(2023, 10, 26, 14, 52, 37, 773000, tzinfo=tzlocal()),\n", + " 'ModelPackageStatus': 'Completed',\n", + " 'ModelApprovalStatus': 'Approved'}" + ] + }, + "execution_count": 692, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "response = sagemaker_client.list_model_packages(\n", + " ModelPackageGroupName=MODEL_PACKAGE_GROUP,\n", + " ModelApprovalStatus=\"Approved\",\n", + " SortBy=\"CreationTime\",\n", + " MaxResults=1,\n", + ")\n", + "\n", + "package = (\n", + " response[\"ModelPackageSummaryList\"][0]\n", + " if response[\"ModelPackageSummaryList\"]\n", + " else None\n", + ")\n", + "package" + ] + }, + { + "cell_type": "markdown", + "id": "af752269", + "metadata": {}, + "source": [ + "We can now create a [Model Package](https://sagemaker.readthedocs.io/en/stable/api/inference/model.html#sagemaker.model.ModelPackage) using the ARN of the model from the Model Registry:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 693, + "id": "dee516e9", + "metadata": {}, + "outputs": [], + "source": [ + "from sagemaker import ModelPackage\n", + "\n", + "model_package = ModelPackage(\n", + " model_package_arn=package[\"ModelPackageArn\"],\n", + " sagemaker_session=sagemaker_session,\n", + " role=role,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "b3119b48-2ddf-40b5-9ac0-680073a53d06", + "metadata": {}, + "source": [ + "Let's now deploy the model to an endpoint:\n" + ] + }, + { + "cell_type": "markdown", + "id": "fbf2ed9f", + "metadata": {}, + "source": [ + "#| hide\n", + "\n", + "
Note: \n", + " The %%script cell magic is a convenient way to prevent the notebook from executing a specific cell. If you want to run the cell, comment out the line containing the %%script cell magic.\n", + "
\n" + ] + }, + { + "cell_type": "code", + "execution_count": 694, + "id": "7c8852d5-818a-406c-944d-30bf6de90288", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "%%script false --no-raise-error\n", + "#| eval: false\n", + "\n", + "model_package.deploy(\n", + " endpoint_name=ENDPOINT, \n", + " initial_instance_count=1, \n", + " instance_type=config[\"instance_type\"]\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "3dd7a725", + "metadata": {}, + "source": [ + "After deploying the model, we can test the endpoint to make sure it works.\n", + "\n", + "Each line of the payload we'll send to the endpoint contains the information of a penguin. Notice the model expects data that's already transformed. We can't provide the original data from our dataset because the model we registered will not work with it.\n", + "\n", + "The endpoint will return the predictions for each of these lines.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 695, + "id": "ba7da291", + "metadata": {}, + "outputs": [], + "source": [ + "payload = \"\"\"\n", + "0.6569590202313976,-1.0813829646495108,1.2097102831892812,0.9226343641317372,1.0,0.0,0.0\n", + "-0.7751048801481084,0.8822689351285553,-1.2168066120762704,0.9226343641317372,0.0,1.0,0.0\n", + "-0.837387834894918,0.3386660813829646,-0.26237731892812,-1.92351941317372,0.0,0.0,1.0\n", + "\"\"\"" + ] + }, + { + "cell_type": "markdown", + "id": "30bcfffa-0ba6-4ad8-8b4f-1ea19b35a22f", + "metadata": {}, + "source": [ + "Let's send the payload to the endpoint and print its response:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 696, + "id": "0817a25e-8224-4911-830b-d659e7458b4a", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "An error occurred (ValidationError) when calling the InvokeEndpoint operation: Endpoint penguins-endpoint of account 325223348818 not found.\n" + ] + } + ], + "source": [ + "predictor = Predictor(endpoint_name=ENDPOINT)\n", + "\n", + "try:\n", + " response = predictor.predict(payload, initial_args={\"ContentType\": \"text/csv\"})\n", + " response = json.loads(response.decode(\"utf-8\"))\n", + "\n", + " print(json.dumps(response, indent=2))\n", + " print(f\"\\nSpecies: {np.argmax(response['predictions'], axis=1)}\")\n", + "except Exception as e:\n", + " print(e)" + ] + }, + { + "cell_type": "markdown", + "id": "28f5d383-fcd7-454c-bbd6-ce4ce7b2104a", + "metadata": {}, + "source": [ + "After testing the endpoint, we need to ensure we delete it:\n" + ] + }, + { + "cell_type": "markdown", + "id": "d9ec7eeb", + "metadata": {}, + "source": [ + "#| hide\n", + "\n", + "
Note: \n", + " The %%script cell magic is a convenient way to prevent the notebook from executing a specific cell. If you want to run the cell, comment out the line containing the %%script cell magic.\n", + "
" + ] + }, + { + "cell_type": "code", + "execution_count": 697, + "id": "6b32c3a4-312e-473c-a217-33606f77d1e9", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "%%script false --no-raise-error\n", + "#| eval: false\n", + "#| code: true\n", + "#| output: false\n", + "\n", + "predictor.delete_endpoint()" + ] + }, + { + "cell_type": "markdown", + "id": "99b90cce", + "metadata": {}, + "source": [ + "Deploying the model we trained directly to an endpoint doesn't lets us control the data that goes in and comes out of the endpoint. The TensorFlow model we trained requires transformed data, which makes it useless to other applications. Fortunately, we can create an Inference Pipeline using SageMaker to control the data that goes in and comes out of the endpoint.\n", + "\n", + "Our inference pipeline will have three components:\n", + "\n", + "1. A preprocessing transformer that will transform the input data into the format the model expects.\n", + "2. The TensorFlow model we trained.\n", + "3. A postprocessing transformer that will transform the output of the model into a human-readable format.\n", + "\n", + "We want our endpoint to handle unprocessed data in CSV and JSON format and return the penguin's species. Here is an example of the payload input we want the endpoint to support:\n", + "\n", + "```{json}\n", + "{\n", + " \"island\": \"Biscoe\",\n", + " \"culmen_length_mm\": 48.6,\n", + " \"culmen_depth_mm\": 16.0,\n", + " \"flipper_length_mm\": 230.0,\n", + " \"body_mass_g\": 5800.0,\n", + "}\n", + "```\n", + "\n", + "And here is an example of the output we'd like to get from the endpoint:\n", + "\n", + "```{json}\n", + "{\n", + " \"prediction\": \"Adelie\",\n", + " \"confidence\": 0.802672\n", + "}\n", + "```\n" + ] + }, + { + "cell_type": "markdown", + "id": "0d0838bf", + "metadata": {}, + "source": [ + "### Step 2 - Creating the Preprocessing Script\n", + "\n", + "The first component of our inference pipeline will transform the input data into the format the model expects. We'll use the Scikit-Learn transformer we saved when we split and transformed the data. To deploy this component as part of an inference pipeline, we need to write a script that loads the transformer, uses it to modify the input data, and returns the output in the format the TensorFlow model expects.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 698, + "id": "e2d61d5c", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Overwriting code/inference/preprocessing_component.py\n" + ] + } + ], + "source": [ + "%%writefile {INFERENCE_CODE_FOLDER}/preprocessing_component.py\n", + "\n", + "#| label: preprocessing-component\n", + "#| echo: true\n", + "#| output: false\n", + "#| filename: preprocessing_component.py\n", + "#| code-line-numbers: true\n", + "\n", + "import os\n", + "import numpy as np\n", + "import pandas as pd\n", + "import json\n", + "import joblib\n", + "\n", + "from io import StringIO\n", + "\n", + "try:\n", + " from sagemaker_containers.beta.framework import encoders, worker\n", + "except ImportError:\n", + " # We don't have access to the `worker` instance when testing locally. \n", + " # We'll set it to None so we can change the way functions create a response.\n", + " worker = None\n", + "\n", + "\n", + "TARGET_COLUMN = \"species\"\n", + "FEATURE_COLUMNS = [\n", + " \"island\",\n", + " \"culmen_length_mm\",\n", + " \"culmen_depth_mm\", \n", + " \"flipper_length_mm\",\n", + " \"body_mass_g\",\n", + " \"sex\"\n", + "]\n", + "\n", + "\n", + "def input_fn(input_data, content_type):\n", + " \"\"\"\n", + " Parses the input payload and creates a Pandas DataFrame.\n", + " \n", + " This function will check whether the target column is present in the\n", + " input data, and will remove it.\n", + " \"\"\"\n", + " \n", + " if content_type == \"text/csv\":\n", + " df = pd.read_csv(StringIO(input_data), header=None, skipinitialspace=True)\n", + "\n", + " if len(df.columns) == len(FEATURE_COLUMNS) + 1:\n", + " df = df.drop(df.columns[0], axis=1)\n", + " \n", + " df.columns = FEATURE_COLUMNS\n", + " return df\n", + " \n", + " if content_type == \"application/json\":\n", + " df = pd.DataFrame([json.loads(input_data)])\n", + " \n", + " if \"species\" in df.columns:\n", + " df = df.drop(\"species\", axis=1)\n", + " \n", + " return df\n", + " \n", + " else:\n", + " raise ValueError(f\"{content_type} is not supported.!\")\n", + "\n", + "\n", + "def output_fn(prediction, accept):\n", + " \"\"\"\n", + " Formats the prediction output to generate a response.\n", + " \n", + " The default accept/content-type between containers for serial inference is JSON. \n", + " Since this model will preceed a TensorFlow model, we want to return a JSON object\n", + " following TensorFlow's input requirements.\n", + " \"\"\"\n", + " \n", + " if prediction is None:\n", + " raise Exception(f\"There was an error transforming the input data\")\n", + "\n", + " if accept == \"text/csv\":\n", + " return worker.Response(encoders.encode(prediction, accept), mimetype=accept) if worker else prediction, accept \n", + " \n", + " if accept == \"application/json\":\n", + " instances = [p for p in prediction.tolist()]\n", + " response = {\"instances\": instances}\n", + " return worker.Response(json.dumps(response), mimetype=accept) if worker else (response, accept)\n", + "\n", + " raise Exception(f\"{accept} accept type is not supported.\")\n", + "\n", + "\n", + "def predict_fn(input_data, model):\n", + " \"\"\"\n", + " Preprocess the input using the transformer.\n", + " \"\"\"\n", + " \n", + " try:\n", + " response = model.transform(input_data)\n", + " return response\n", + " except ValueError as e:\n", + " print(\"Error transforming the input data\", e)\n", + " return None\n", + "\n", + "\n", + "def model_fn(model_dir):\n", + " \"\"\"\n", + " Deserializes the model that will be used in this container.\n", + " \"\"\"\n", + " \n", + " return joblib.load(os.path.join(model_dir, \"features.joblib\"))" + ] + }, + { + "cell_type": "markdown", + "id": "037982c1", + "metadata": {}, + "source": [ + "Let's test the script to ensure everything is working as expected:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 699, + "id": "33893ef2", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m [100%]\u001b[0m\n", + "\u001b[32m\u001b[32m\u001b[1m10 passed\u001b[0m\u001b[32m in 0.07s\u001b[0m\u001b[0m\n" + ] + } + ], + "source": [ + "%%ipytest\n", + "#| code-fold: true\n", + "#| output: false\n", + "\n", + "from preprocessing_component import input_fn, predict_fn, output_fn, model_fn\n", + "\n", + "\n", + "@pytest.fixture(scope=\"function\", autouse=False)\n", + "def directory():\n", + " directory = tempfile.mkdtemp()\n", + " input_directory = Path(directory) / \"input\"\n", + " input_directory.mkdir(parents=True, exist_ok=True)\n", + " shutil.copy2(DATA_FILEPATH, input_directory / \"data.csv\")\n", + " \n", + " directory = Path(directory)\n", + " \n", + " preprocess(base_directory=directory)\n", + " \n", + " with tarfile.open(directory / \"model\" / \"model.tar.gz\") as tar:\n", + " tar.extractall(path=directory / \"model\")\n", + " \n", + " yield directory / \"model\"\n", + " \n", + " shutil.rmtree(directory)\n", + "\n", + "\n", + "\n", + "def test_input_csv_drops_target_column_if_present():\n", + " input_data = \"\"\"\n", + " Adelie, Torgersen, 39.1, 18.7, 181, 3750, MALE\n", + " \"\"\"\n", + " \n", + " df = input_fn(input_data, \"text/csv\")\n", + " assert len(df.columns) == 6 and \"species\" not in df.columns\n", + "\n", + "\n", + "def test_input_json_drops_target_column_if_present():\n", + " input_data = json.dumps({\n", + " \"species\": \"Adelie\", \n", + " \"island\": \"Torgersen\",\n", + " \"culmen_length_mm\": 44.1,\n", + " \"culmen_depth_mm\": 18.0,\n", + " \"flipper_length_mm\": 210.0,\n", + " \"body_mass_g\": 4000.0,\n", + " \"sex\": \"MALE\"\n", + " })\n", + " \n", + " df = input_fn(input_data, \"application/json\")\n", + " assert len(df.columns) == 6 and \"species\" not in df.columns\n", + "\n", + "\n", + "def test_input_csv_works_without_target_column():\n", + " input_data = \"\"\"\n", + " Torgersen, 39.1, 18.7, 181, 3750, MALE\n", + " \"\"\"\n", + " \n", + " df = input_fn(input_data, \"text/csv\")\n", + " assert len(df.columns) == 6\n", + "\n", + "\n", + "def test_input_json_works_without_target_column():\n", + " input_data = json.dumps({\n", + " \"island\": \"Torgersen\",\n", + " \"culmen_length_mm\": 44.1,\n", + " \"culmen_depth_mm\": 18.0,\n", + " \"flipper_length_mm\": 210.0,\n", + " \"body_mass_g\": 4000.0,\n", + " \"sex\": \"MALE\"\n", + " })\n", + " \n", + " df = input_fn(input_data, \"application/json\")\n", + " assert len(df.columns) == 6\n", + "\n", + "\n", + "def test_output_csv_raises_exception_if_prediction_is_none():\n", + " with pytest.raises(Exception):\n", + " output_fn(None, \"text/csv\")\n", + " \n", + " \n", + "def test_output_json_raises_exception_if_prediction_is_none():\n", + " with pytest.raises(Exception):\n", + " output_fn(None, \"application/json\")\n", + " \n", + " \n", + "def test_output_csv_returns_prediction():\n", + " prediction = np.array([\n", + " [-1.3944109908736013,1.15488062669371,-0.7954340636549508,-0.5536447804097907,0.0,1.0,0.0],\n", + " [1.0557485835338234,0.5040085971987002,-0.5824506029515057,-0.5851840035995248,0.0,1.0,0.0]\n", + " ])\n", + " \n", + " response = output_fn(prediction, \"text/csv\")\n", + " \n", + " assert response == (prediction, \"text/csv\")\n", + " \n", + " \n", + "def test_output_json_returns_tensorflow_ready_input():\n", + " prediction = np.array([\n", + " [-1.3944109908736013,1.15488062669371,-0.7954340636549508,-0.5536447804097907,0.0,1.0,0.0],\n", + " [1.0557485835338234,0.5040085971987002,-0.5824506029515057,-0.5851840035995248,0.0,1.0,0.0]\n", + " ])\n", + " \n", + " response = output_fn(prediction, \"application/json\")\n", + " \n", + " assert response[0] == {\n", + " \"instances\": [\n", + " [-1.3944109908736013,1.15488062669371,-0.7954340636549508,-0.5536447804097907,0.0,1.0,0.0],\n", + " [1.0557485835338234,0.5040085971987002,-0.5824506029515057,-0.5851840035995248,0.0,1.0,0.0]\n", + " ]\n", + " }\n", + " \n", + " assert response[1] == \"application/json\"\n", + "\n", + " \n", + "def test_predict_transforms_data(directory):\n", + " input_data = \"\"\"\n", + " Torgersen, 39.1, 18.7, 181, 3750, MALE\n", + " \"\"\"\n", + " \n", + " model = model_fn(str(directory))\n", + " df = input_fn(input_data, \"text/csv\")\n", + " response = predict_fn(df, model)\n", + " assert type(response) is np.ndarray\n", + " \n", + "\n", + "def test_predict_returns_none_if_invalid_input(directory):\n", + " input_data = \"\"\"\n", + " Invalid, 39.1, 18.7, 181, 3750, MALE\n", + " \"\"\"\n", + " \n", + " model = model_fn(str(directory))\n", + " df = input_fn(input_data, \"text/csv\")\n", + " assert predict_fn(df, model) is None" + ] + }, + { + "cell_type": "markdown", + "id": "8eacf7aa", + "metadata": {}, + "source": [ + "### Step 3 - Creating the Postprocessing Script\n", + "\n", + "The final component of our inference pipeline will transform the output from the model into a human-readable format. We'll use the Scikit-Learn target transformer we saved when we split and transformed the data. To deploy this component as part of an inference pipeline, we need to write a script that loads the transformer, uses it to modify the output from the model, and returns a human-readable format.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 700, + "id": "48c69002", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Overwriting code/inference/postprocessing_component.py\n" + ] + } + ], + "source": [ + "%%writefile {INFERENCE_CODE_FOLDER}/postprocessing_component.py\n", + "\n", + "#| label: postprocessing-component\n", + "#| echo: true\n", + "#| output: false\n", + "#| filename: postprocessing_component.py\n", + "#| code-line-numbers: true\n", + "\n", + "import os\n", + "import numpy as np\n", + "import pandas as pd\n", + "import argparse\n", + "import json\n", + "import tarfile\n", + "import joblib\n", + "\n", + "from pathlib import Path\n", + "from io import StringIO\n", + "\n", + "from sklearn.compose import ColumnTransformer, make_column_selector\n", + "from sklearn.impute import SimpleImputer\n", + "from sklearn.pipeline import Pipeline, make_pipeline\n", + "from sklearn.preprocessing import OneHotEncoder, LabelEncoder, StandardScaler, OrdinalEncoder\n", + "from pickle import dump, load\n", + "\n", + "\n", + "try:\n", + " from sagemaker_containers.beta.framework import encoders, worker\n", + "except ImportError:\n", + " # We don't have access to the `worker` instance when testing locally. \n", + " # We'll set it to None so we can change the way functions create a response.\n", + " worker = None\n", + "\n", + "\n", + "def input_fn(input_data, content_type):\n", + " if content_type == \"application/json\":\n", + " predictions = json.loads(input_data)[\"predictions\"]\n", + " return predictions\n", + " \n", + " else:\n", + " raise ValueError(f\"{content_type} is not supported.!\")\n", + "\n", + "\n", + "def output_fn(prediction, accept):\n", + " if accept == \"text/csv\":\n", + " return worker.Response(encoders.encode(prediction, accept), mimetype=accept) if worker else (prediction, accept)\n", + " \n", + " if accept == \"application/json\":\n", + " response = []\n", + " for p, c in prediction:\n", + " response.append({\n", + " \"prediction\": p,\n", + " \"confidence\": c\n", + " })\n", + "\n", + " # If there's only one prediction, we'll return it\n", + " # as a single object.\n", + " if len(response) == 1:\n", + " response = response[0]\n", + " \n", + " return worker.Response(json.dumps(response), mimetype=accept) if worker else (response, accept)\n", + " \n", + " raise RuntimeException(f\"{accept} accept type is not supported.\")\n", + "\n", + "\n", + "def predict_fn(input_data, model):\n", + " \"\"\"\n", + " Transforms the prediction into its corresponding category.\n", + " \"\"\"\n", + "\n", + " predictions = np.argmax(input_data, axis=-1)\n", + " confidence = np.max(input_data, axis=-1)\n", + " return [(model[prediction], confidence) for confidence, prediction in zip(confidence, predictions)]\n", + "\n", + "\n", + "def model_fn(model_dir):\n", + " \"\"\"\n", + " Deserializes the target model and returns the list of fitted categories.\n", + " \"\"\"\n", + " \n", + " model = joblib.load(os.path.join(model_dir, \"target.joblib\"))\n", + " return model.named_transformers_[\"species\"].categories_[0]" + ] + }, + { + "cell_type": "markdown", + "id": "86c421c7", + "metadata": {}, + "source": [ + "Let's test the script to ensure everything is working as expected:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 701, + "id": "741b8402", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m [100%]\u001b[0m\n", + "\u001b[32m\u001b[32m\u001b[1m3 passed\u001b[0m\u001b[32m in 0.01s\u001b[0m\u001b[0m\n" + ] + } + ], + "source": [ + "%%ipytest\n", + "#| code-fold: true\n", + "#| output: false\n", + "\n", + "import numpy as np\n", + "\n", + "from postprocessing_component import predict_fn, output_fn\n", + "\n", + "\n", + "def test_predict_returns_prediction_as_first_column():\n", + " input_data = [\n", + " [0.6, 0.2, 0.2], \n", + " [0.1, 0.8, 0.1],\n", + " [0.2, 0.1, 0.7]\n", + " ]\n", + " \n", + " categories = [\"Adelie\", \"Gentoo\", \"Chinstrap\"]\n", + " \n", + " response = predict_fn(input_data, categories)\n", + " \n", + " assert response == [\n", + " (\"Adelie\", 0.6),\n", + " (\"Gentoo\", 0.8),\n", + " (\"Chinstrap\", 0.7)\n", + " ]\n", + "\n", + "\n", + "def test_output_does_not_return_array_if_single_prediction():\n", + " prediction = [(\"Adelie\", 0.6)]\n", + " response, _ = output_fn(prediction, \"application/json\")\n", + "\n", + " assert response[\"prediction\"] == \"Adelie\"\n", + "\n", + "\n", + "def test_output_returns_array_if_multiple_predictions():\n", + " prediction = [(\"Adelie\", 0.6), (\"Gentoo\", 0.8)]\n", + " response, _ = output_fn(prediction, \"application/json\")\n", + "\n", + " assert len(response) == 2\n", + " assert response[0][\"prediction\"] == \"Adelie\"\n", + " assert response[1][\"prediction\"] == \"Gentoo\"\n" + ] + }, + { + "cell_type": "markdown", + "id": "5e5526e5", + "metadata": {}, + "source": [ + "### Step 4 - Setting up the Inference Pipeline\n", + "\n", + "We can now create a [PipelineModel](https://sagemaker.readthedocs.io/en/stable/api/inference/pipeline.html#sagemaker.pipeline.PipelineModel) to define our inference pipeline.\n" + ] + }, + { + "cell_type": "markdown", + "id": "2baf91d8", + "metadata": {}, + "source": [ + "We'll use the model we generated from the first step of the pipeline as the input to the first and last components of the inference pipeline. This `model.tar.gz` file contains the two transformers we need to preprocess and postprocess the data. Let's create a variable with the URI to this file:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 702, + "id": "53ea0ccf", + "metadata": {}, + "outputs": [], + "source": [ + "transformation_pipeline_model = Join(\n", + " on=\"/\",\n", + " values=[\n", + " split_and_transform_data_step.properties.ProcessingOutputConfig.Outputs[\n", + " \"model\"\n", + " ].S3Output.S3Uri,\n", + " \"model.tar.gz\",\n", + " ],\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "1b7119a4", + "metadata": {}, + "source": [ + "Here is the first component of the inference pipeline. It will preprocess the data before sending it to the TensorFlow model:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 703, + "id": "11a0effd", + "metadata": {}, + "outputs": [], + "source": [ + "from sagemaker.sklearn.model import SKLearnModel\n", + "\n", + "preprocessing_model = SKLearnModel(\n", + " model_data=transformation_pipeline_model,\n", + " entry_point=\"preprocessing_component.py\",\n", + " source_dir=str(INFERENCE_CODE_FOLDER),\n", + " framework_version=\"1.2-1\",\n", + " sagemaker_session=config[\"session\"],\n", + " role=role,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "26a18bfb", + "metadata": {}, + "source": [ + "Here is the last component of the inference pipeline. It will postprocess the output from the TensorFlow model before sending it back to the user:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 704, + "id": "5d7a5926", + "metadata": {}, + "outputs": [], + "source": [ + "post_processing_model = SKLearnModel(\n", + " model_data=transformation_pipeline_model,\n", + " entry_point=\"postprocessing_component.py\",\n", + " source_dir=str(INFERENCE_CODE_FOLDER),\n", + " framework_version=\"1.2-1\",\n", + " sagemaker_session=config[\"session\"],\n", + " role=role,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "2918f505", + "metadata": {}, + "source": [ + "We can now create the inference pipeline using the three models:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 705, + "id": "157b8858", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "from sagemaker.pipeline import PipelineModel\n", + "\n", + "pipeline_model = PipelineModel(\n", + " name=\"inference-model\",\n", + " models=[preprocessing_model, tensorflow_model, post_processing_model],\n", + " sagemaker_session=config[\"session\"],\n", + " role=role,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "501cdaa1", + "metadata": {}, + "source": [ + "### Step 5 - Registering the Model\n", + "\n", + "We'll modify the pipeline to register the Pipeline Model in the Model Registry. We'll use a different group name to keep Pipeline Models separate.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 706, + "id": "aefe580a", + "metadata": {}, + "outputs": [], + "source": [ + "PIPELINE_MODEL_PACKAGE_GROUP = \"pipeline\"" + ] + }, + { + "cell_type": "markdown", + "id": "77b2b06e", + "metadata": {}, + "source": [ + "Let's now register the model. Notice that we will register the model with \"PendingManualApproval\" status. This means that we'll need to manually approve the model before it can be deployed to an endpoint. Check [Register a Model Version](https://docs.aws.amazon.com/sagemaker/latest/dg/model-registry-version.html) for more information about model registration.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 707, + "id": "f84d2cd5", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/svpino/dev/ml.school/.venv/lib/python3.9/site-packages/sagemaker/workflow/pipeline_context.py:297: UserWarning: Running within a PipelineSession, there will be No Wait, No Logs, and No Job being started.\n", + " warnings.warn(\n" + ] + } + ], + "source": [ + "# | code: true\n", + "# | output: false\n", + "\n", + "register_model_step = ModelStep(\n", + " name=\"register\",\n", + " display_name=\"register-model\",\n", + " step_args=pipeline_model.register(\n", + " model_package_group_name=PIPELINE_MODEL_PACKAGE_GROUP,\n", + " model_metrics=model_metrics,\n", + " approval_status=\"PendingManualApproval\",\n", + " # Our inference pipeline model supports two content\n", + " # types: text/csv and application/json.\n", + " content_types=[\"text/csv\", \"application/json\"],\n", + " response_types=[\"text/csv\", \"application/json\"],\n", + " # This is the suggested inference instance types when\n", + " # deploying the model or using it as part of a batch\n", + " # transform job.\n", + " inference_instances=[\"ml.m5.xlarge\"],\n", + " transform_instances=[\"ml.m5.xlarge\"],\n", + " domain=\"MACHINE_LEARNING\",\n", + " task=\"CLASSIFICATION\",\n", + " framework=\"TENSORFLOW\",\n", + " framework_version=config[\"framework_version\"],\n", + " ),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "c00a4ebb-fb21-4935-9d7b-9500e47e07f9", + "metadata": {}, + "source": [ + "### Step 6 - Modifying the Condition Step\n", + "\n", + "Since we modified the registration step, we also need to modify the Condition Step to use the new registration:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 708, + "id": "b9712905-9fe3-4148-ae6d-05b0a48e742e", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "condition_step = ConditionStep(\n", + " name=\"check-model-accuracy\",\n", + " conditions=[condition],\n", + " if_steps=[register_model_step] if not LOCAL_MODE else [],\n", + " else_steps=[fail_step],\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "0730a388", + "metadata": {}, + "source": [ + "### Step 7 - Creating the Pipeline\n", + "\n", + "We can now define the SageMaker Pipeline and submit its definition to the SageMaker Pipelines service to create the pipeline if it doesn't exist or update it if it does.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 709, + "id": "bad9f51d", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO:sagemaker.image_uris:image_uri is not presented, retrieving image_uri based on instance_type, framework etc.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Using provided s3_resource\n", + "Using provided s3_resource\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO:sagemaker.processing:Uploaded None to s3://mlschool/session4-pipeline/code/09fea667a5ab7c37a068f22c00762d0b/sourcedir.tar.gz\n", + "INFO:sagemaker.processing:runproc.sh uploaded to s3://mlschool/session4-pipeline/code/2c207c809cb0e0e9a1d77e5247f961f9/runproc.sh\n", + "WARNING:sagemaker.workflow._utils:Popping out 'CertifyForMarketplace' from the pipeline definition since it will be overridden in pipeline execution time.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO:sagemaker.image_uris:image_uri is not presented, retrieving image_uri based on instance_type, framework etc.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Using provided s3_resource\n", + "Using provided s3_resource\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO:sagemaker.processing:Uploaded None to s3://mlschool/session4-pipeline/code/09fea667a5ab7c37a068f22c00762d0b/sourcedir.tar.gz\n", + "INFO:sagemaker.processing:runproc.sh uploaded to s3://mlschool/session4-pipeline/code/2c207c809cb0e0e9a1d77e5247f961f9/runproc.sh\n" + ] + }, + { + "data": { + "text/plain": [ + "{'PipelineArn': 'arn:aws:sagemaker:us-east-1:325223348818:pipeline/session4-pipeline',\n", + " 'ResponseMetadata': {'RequestId': '2cd65edc-9bad-4b67-a1d2-aa22698d6a39',\n", + " 'HTTPStatusCode': 200,\n", + " 'HTTPHeaders': {'x-amzn-requestid': '2cd65edc-9bad-4b67-a1d2-aa22698d6a39',\n", + " 'content-type': 'application/x-amz-json-1.1',\n", + " 'content-length': '85',\n", + " 'date': 'Fri, 27 Oct 2023 14:38:46 GMT'},\n", + " 'RetryAttempts': 0}}" + ] + }, + "execution_count": 709, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# | code: true\n", + "# | output: false\n", + "\n", + "session4_pipeline = Pipeline(\n", + " name=\"session4-pipeline\",\n", + " parameters=[dataset_location, accuracy_threshold],\n", + " steps=[\n", + " split_and_transform_data_step,\n", + " tune_model_step if USE_TUNING_STEP else train_model_step,\n", + " evaluate_model_step,\n", + " condition_step,\n", + " ],\n", + " pipeline_definition_config=pipeline_definition_config,\n", + " sagemaker_session=config[\"session\"],\n", + ")\n", + "\n", + "session4_pipeline.upsert(role_arn=role)" + ] + }, + { + "cell_type": "markdown", + "id": "20c71f91", + "metadata": {}, + "source": [ + "We can now start the pipeline:\n" + ] + }, + { + "cell_type": "markdown", + "id": "34819536", + "metadata": {}, + "source": [ + "#| hide\n", + "\n", + "
Note: \n", + " The %%script cell magic is a convenient way to prevent the notebook from executing a specific cell. If you want to run the cell, comment out the line containing the %%script cell magic.\n", + "
\n" + ] + }, + { + "cell_type": "code", + "execution_count": 710, + "id": "20dfbd97", + "metadata": {}, + "outputs": [], + "source": [ + "%%script false --no-raise-error\n", + "\n", + "#| eval: false\n", + "#| code: true\n", + "#| output: false\n", + "\n", + "session4_pipeline.start()" + ] + }, + { + "cell_type": "markdown", + "id": "2c74cc70", + "metadata": {}, + "source": [ + "### Step 8 - Creating the Lambda Function\n", + "\n", + "We will use [Amazon EventBridge](https://aws.amazon.com/pm/eventbridge/) to trigger a Lambda function that will deploy the model whenever its status changes from \"PendingManualApproval\" to \"Approved.\" Let's start by writing the Lambda function to take the model information and create a new endpoint.\n", + "\n", + "We'll enable [Data Capture](https://docs.aws.amazon.com/sagemaker/latest/dg/model-monitor-data-capture.html) as part of the endpoint configuration. With Data Capture we can record the inputs and outputs of the endpoint to use them later for monitoring the model:\n", + "\n", + "- `InitialSamplingPercentage` represents the percentage of traffic that we want to capture.\n", + "- `DestinationS3Uri` specifies the S3 location where we want to store the captured data.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 744, + "id": "998314a3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Overwriting code/lambda.py\n" + ] + } + ], + "source": [ + "%%writefile {CODE_FOLDER}/lambda.py\n", + "\n", + "import os\n", + "import json\n", + "import boto3\n", + "import time\n", + "\n", + "sagemaker = boto3.client(\"sagemaker\")\n", + "\n", + "def lambda_handler(event, context):\n", + " model_package_arn = event[\"detail\"][\"ModelPackageArn\"]\n", + " approval_status = event[\"detail\"][\"ModelApprovalStatus\"]\n", + "\n", + " print(f\"Model: {model_package_arn}\")\n", + " print(f\"Approval status: {approval_status}\")\n", + " \n", + " # We only want to deploy the approved models\n", + " if approval_status != \"Approved\":\n", + " response = {\n", + " \"message\": \"Skipping deployment.\",\n", + " \"approval_status\": approval_status,\n", + " }\n", + "\n", + " print(response)\n", + " return {\n", + " \"statusCode\": 200,\n", + " \"body\": json.dumps(response)\n", + " } \n", + " \n", + " endpoint_name = os.environ[\"ENDPOINT\"]\n", + " data_capture_destination = os.environ[\"DATA_CAPTURE_DESTINATION\"]\n", + " role = os.environ[\"ROLE\"]\n", + " \n", + " timestamp = time.strftime(\"%m%d%H%M%S\", time.localtime())\n", + " model_name = f\"{endpoint_name}-model-{timestamp}\"\n", + " endpoint_config_name = f\"{endpoint_name}-config-{timestamp}\"\n", + "\n", + " sagemaker.create_model(\n", + " ModelName=model_name, \n", + " ExecutionRoleArn=role, \n", + " Containers=[{\n", + " \"ModelPackageName\": model_package_arn\n", + " }] \n", + " )\n", + "\n", + " sagemaker.create_endpoint_config(\n", + " EndpointConfigName=endpoint_config_name,\n", + " ProductionVariants=[{\n", + " \"ModelName\": model_name,\n", + " \"InstanceType\": \"ml.m5.xlarge\",\n", + " \"InitialVariantWeight\": 1,\n", + " \"InitialInstanceCount\": 1,\n", + " \"VariantName\": \"AllTraffic\",\n", + " }],\n", + "\n", + " # We can enable Data Capture to record the inputs and outputs\n", + " # of the endpoint to use them later for monitoring the model. \n", + " DataCaptureConfig={\n", + " \"EnableCapture\": True,\n", + " \"InitialSamplingPercentage\": 100,\n", + " \"DestinationS3Uri\": data_capture_destination,\n", + " \"CaptureOptions\": [\n", + " {\n", + " \"CaptureMode\": \"Input\"\n", + " },\n", + " {\n", + " \"CaptureMode\": \"Output\"\n", + " },\n", + " ],\n", + " \"CaptureContentTypeHeader\": {\n", + " \"CsvContentTypes\": [\n", + " \"text/csv\",\n", + " \"application/octect-stream\"\n", + " ],\n", + " \"JsonContentTypes\": [\n", + " \"application/json\",\n", + " \"application/octect-stream\"\n", + " ]\n", + " }\n", + " },\n", + " )\n", + " \n", + " response = sagemaker.list_endpoints(NameContains=endpoint_name, MaxResults=1)\n", + "\n", + " if len(response[\"Endpoints\"]) == 0:\n", + " # If the endpoint doesn't exist, let's create it.\n", + " sagemaker.create_endpoint(\n", + " EndpointName=endpoint_name, \n", + " EndpointConfigName=endpoint_config_name,\n", + " )\n", + " else:\n", + " # If the endpoint already exist, let's update it with the\n", + " # new configuration.\n", + " sagemaker.update_endpoint(\n", + " EndpointName=endpoint_name, \n", + " EndpointConfigName=endpoint_config_name,\n", + " )\n", + " \n", + " return {\n", + " \"statusCode\": 200,\n", + " \"body\": json.dumps(\"Endpoint deployed successfully\")\n", + " }" + ] + }, + { + "cell_type": "markdown", + "id": "5b582ace", + "metadata": {}, + "source": [ + "We need to ensure our Lambda function has permission to interact with SageMaker, so let's create a new role and then create the lambda function.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 745, + "id": "4ad4f1f2", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Role \"lambda-deployment-role\" created with ARN \"arn:aws:iam::325223348818:role/lambda-deployment-role\".\n" + ] + } + ], + "source": [ + "#| code: true\n", + "#| output: false\n", + "\n", + "lambda_role_name = \"lambda-deployment-role\"\n", + "lambda_role_arn = None\n", + "\n", + "try:\n", + " response = iam_client.create_role(\n", + " RoleName=lambda_role_name,\n", + " AssumeRolePolicyDocument=json.dumps(\n", + " {\n", + " \"Version\": \"2012-10-17\",\n", + " \"Statement\": [\n", + " {\n", + " \"Effect\": \"Allow\",\n", + " \"Principal\": {\n", + " \"Service\": [\"lambda.amazonaws.com\", \"events.amazonaws.com\"]\n", + " },\n", + " \"Action\": \"sts:AssumeRole\",\n", + " }\n", + " ],\n", + " }\n", + " ),\n", + " Description=\"Lambda Endpoint Deployment\",\n", + " )\n", + "\n", + " lambda_role_arn = response[\"Role\"][\"Arn\"]\n", + "\n", + " iam_client.attach_role_policy(\n", + " PolicyArn=\"arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole\",\n", + " RoleName=lambda_role_name,\n", + " )\n", + "\n", + " iam_client.attach_role_policy(\n", + " PolicyArn=\"arn:aws:iam::aws:policy/AmazonSageMakerFullAccess\",\n", + " RoleName=lambda_role_name,\n", + " )\n", + "\n", + " print(f'Role \"{lambda_role_name}\" created with ARN \"{lambda_role_arn}\".')\n", + "except iam_client.exceptions.EntityAlreadyExistsException:\n", + " response = iam_client.get_role(RoleName=lambda_role_name)\n", + " lambda_role_arn = response[\"Role\"][\"Arn\"]\n", + " print(f'Role \"{lambda_role_name}\" already exists with ARN \"{lambda_role_arn}\".')\n" + ] + }, + { + "cell_type": "markdown", + "id": "acef9d48", + "metadata": {}, + "source": [ + "We can now create the Lambda function:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 747, + "id": "ad8c8019", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{'ResponseMetadata': {'RequestId': '57179d72-6fc2-49cc-9326-cb87bd63bda1',\n", + " 'HTTPStatusCode': 201,\n", + " 'HTTPHeaders': {'date': 'Fri, 27 Oct 2023 16:01:42 GMT',\n", + " 'content-type': 'application/json',\n", + " 'content-length': '1421',\n", + " 'connection': 'keep-alive',\n", + " 'x-amzn-requestid': '57179d72-6fc2-49cc-9326-cb87bd63bda1'},\n", + " 'RetryAttempts': 0},\n", + " 'FunctionName': 'deploy_fn',\n", + " 'FunctionArn': 'arn:aws:lambda:us-east-1:325223348818:function:deploy_fn',\n", + " 'Runtime': 'python3.11',\n", + " 'Role': 'arn:aws:iam::325223348818:role/lambda-deployment-role',\n", + " 'Handler': 'lambda.lambda_handler',\n", + " 'CodeSize': 3194,\n", + " 'Description': '',\n", + " 'Timeout': 600,\n", + " 'MemorySize': 128,\n", + " 'LastModified': '2023-10-27T16:01:42.544+0000',\n", + " 'CodeSha256': 'IkCkE0e46WsdhSUEPRlsqEH/6nHhU5laPpgn308D30k=',\n", + " 'Version': '$LATEST',\n", + " 'Environment': {'Variables': {'ROLE': 'arn:aws:iam::325223348818:role/service-role/AmazonSageMaker-ExecutionRole-20230312T160501',\n", + " 'DATA_CAPTURE_DESTINATION': 's3://mlschool/penguins/monitoring/data-capture',\n", + " 'ENDPOINT': 'penguins-endpoint'}},\n", + " 'TracingConfig': {'Mode': 'PassThrough'},\n", + " 'RevisionId': '516fef1e-871b-4a52-81e2-a421f3547ec9',\n", + " 'Layers': [],\n", + " 'State': 'Pending',\n", + " 'StateReason': 'The function is being created.',\n", + " 'StateReasonCode': 'Creating',\n", + " 'PackageType': 'Zip',\n", + " 'Architectures': ['x86_64'],\n", + " 'EphemeralStorage': {'Size': 512},\n", + " 'SnapStart': {'ApplyOn': 'None', 'OptimizationStatus': 'Off'},\n", + " 'RuntimeVersionConfig': {'RuntimeVersionArn': 'arn:aws:lambda:us-east-1::runtime:6cf63f1a78b5c5e19617d6b4b111370fdbda415ea91bdfdc5aacef9fee76b64a'}}" + ] + }, + "execution_count": 747, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# | code: true\n", + "# | output: false\n", + "\n", + "from sagemaker.lambda_helper import Lambda\n", + "\n", + "\n", + "deploy_lambda_fn = Lambda(\n", + " function_name=\"deploy_fn\",\n", + " execution_role_arn=lambda_role_arn,\n", + " script=str(CODE_FOLDER / \"lambda.py\"),\n", + " handler=\"lambda.lambda_handler\",\n", + " timeout=600,\n", + " session=sagemaker_session,\n", + " runtime=\"python3.11\",\n", + " environment={\n", + " \"Variables\": {\n", + " \"ENDPOINT\": ENDPOINT,\n", + " \"DATA_CAPTURE_DESTINATION\": DATA_CAPTURE_DESTINATION,\n", + " \"ROLE\": role,\n", + " }\n", + " },\n", + ")\n", + "\n", + "lambda_response = None\n", + "if not LOCAL_MODE:\n", + " lambda_response = deploy_lambda_fn.upsert()\n", + "\n", + "lambda_response" + ] + }, + { + "cell_type": "markdown", + "id": "d4ad06ac", + "metadata": {}, + "source": [ + "### Step 9 - Setting Up EventBridge\n", + "\n", + "We can now create an EventBridge rule that triggers the deployment process whenever a model approval status becomes \"Approved\". To do this, let's define the event pattern that will trigger the deployment process:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 748, + "id": "27ce7cc5", + "metadata": {}, + "outputs": [], + "source": [ + "event_pattern = f\"\"\"\n", + "{{\n", + " \"source\": [\"aws.sagemaker\"],\n", + " \"detail-type\": [\"SageMaker Model Package State Change\"],\n", + " \"detail\": {{\n", + " \"ModelPackageGroupName\": [\"{PIPELINE_MODEL_PACKAGE_GROUP}\"],\n", + " \"ModelApprovalStatus\": [\"Approved\"]\n", + " }}\n", + "}}\n", + "\"\"\"" + ] + }, + { + "cell_type": "markdown", + "id": "d1b23587", + "metadata": {}, + "source": [ + "Let's now create the EventBridge rule:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 749, + "id": "2a878179", + "metadata": {}, + "outputs": [], + "source": [ + "events_client = boto3.client(\"events\")\n", + "rule_response = events_client.put_rule(\n", + " Name=\"PipelineModelApprovedRule\",\n", + " EventPattern=event_pattern,\n", + " State=\"ENABLED\",\n", + " RoleArn=role,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "0b3ba782", + "metadata": {}, + "source": [ + "Now, we need to define the target of the rule. The target will trigger whenever the rule matches an event. In this case, we want to trigger the Lambda function we created before:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 750, + "id": "dc714a97", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "response = events_client.put_targets(\n", + " Rule=\"PipelineModelApprovedRule\",\n", + " Targets=[\n", + " {\n", + " \"Id\": \"1\",\n", + " \"Arn\": lambda_response[\"FunctionArn\"],\n", + " }\n", + " ],\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "400585a1", + "metadata": {}, + "source": [ + "Finally, we need to give the Lambda function permission to be triggered by the EventBridge rule:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 751, + "id": "d74be86b", + "metadata": {}, + "outputs": [], + "source": [ + "lambda_client = boto3.client(\"lambda\")\n", + "try:\n", + " response = lambda_client.add_permission(\n", + " Action=\"lambda:InvokeFunction\",\n", + " FunctionName=lambda_response[\"FunctionName\"],\n", + " Principal=\"events.amazonaws.com\",\n", + " SourceArn=rule_response[\"RuleArn\"],\n", + " StatementId=\"EventBridge\",\n", + " )\n", + "except lambda_client.exceptions.ResourceConflictException as e:\n", + " print(f'Function \"{lambda_response[\"FunctionName\"]}\" already has permissions.')" + ] + }, + { + "cell_type": "markdown", + "id": "7dfe7356-53e8-4ac1-9a7f-3bd51bb739a5", + "metadata": {}, + "source": [ + "### Step 10 - Testing the Endpoint\n", + "\n", + "Let's now test the endpoint we deployed automatically with the pipeline. We will use the function to create a predictor with a JSON encoder and decoder.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 718, + "id": "3cc966fb-b611-417f-a8b8-0c5d2f95252c", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Payload:\n", + "Torgersen,39.1,18.7,181.0,3750.0,MALE\n", + "Torgersen,39.5,17.4,186.0,3800.0,FEMALE\n", + "Torgersen,40.3,18.0,195.0,3250.0,FEMALE\n", + "\n", + "An error occurred (ValidationError) when calling the InvokeEndpoint operation: Endpoint penguins-endpoint of account 325223348818 not found.\n" + ] + } + ], + "source": [ + "from sagemaker.serializers import CSVSerializer\n", + "\n", + "predictor = Predictor(\n", + " endpoint_name=ENDPOINT, \n", + " serializer=CSVSerializer(),\n", + " sagemaker_session=sagemaker_session\n", + ")\n", + "\n", + "data = pd.read_csv(DATA_FILEPATH)\n", + "data = data.drop(\"species\", axis=1)\n", + "\n", + "payload = data.iloc[:3].to_csv(header=False, index=False)\n", + "print(f\"Payload:\\n{payload}\")\n", + "\n", + "try:\n", + " response = predictor.predict(payload, initial_args={\"ContentType\": \"text/csv\"})\n", + " print(response.decode(\"utf-8\"))\n", + "except Exception as e:\n", + " print(e)" + ] + }, + { + "cell_type": "markdown", + "id": "67e883b0", + "metadata": {}, + "source": [ + "Let's delete the endpoint:" + ] + }, + { + "cell_type": "markdown", + "id": "6cffc2b5", + "metadata": {}, + "source": [ + "#| hide\n", + "\n", + "
Note: \n", + " The %%script cell magic is a convenient way to prevent the notebook from executing a specific cell. If you want to run the cell, comment out the line containing the %%script cell magic.\n", + "
" + ] + }, + { + "cell_type": "code", + "execution_count": 719, + "id": "8c3e851a-2416-4a0b-b8a1-c483cde3d776", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "%%script false --no-raise-error\n", + "#| eval: false\n", + "#| code: true\n", + "#| output: false\n", + "\n", + "predictor.delete_endpoint()" + ] + }, + { + "cell_type": "markdown", + "id": "d2b2e88b-0740-4214-a92f-ceba981c7e9c", + "metadata": {}, + "source": [ + "### Assignments\n", + "\n", + "* Assignment 4.1 Every Endpoint has an invocation URL you can use to generate predictions with the model from outside AWS. As part of this assignment, write a simple Python script that will run on your local computer and run a few samples through the Endpoint. You will need your AWS access key and secret to connect to the Endpoint.\n", + "\n", + "* Assignment 4.2 We can use model variants to perform A/B testing between a new model and an old model. Create a function that given the ARN of two models in the Model Registry deploys them to an Endpoint as separate variants. Each variant should receive 50% of the traffic. Write another function that invokes the endpoint by default, but allows the caller to invoke a specific variant if they want to.\n", + "\n", + "* Assignment 4.3 We can use SageMaker Model Shadow Deployments to create shadow variants to validate a new model version before promoting it to production. Write a function that given the ARN of a model in the Model Registry, updates an Endpoint and deploys the model as a shadow variant. Check [Shadow variants](https://docs.aws.amazon.com/sagemaker/latest/dg/model-shadow-deployment.html) for more information about this topic. Send some traffic to the Endpoint and compare the results from the main model with its shadow variant.\n", + "\n", + "* Assignment 4.4 SageMaker supports auto scaling your models. Auto scaling dynamically adjusts the number of instances provisioned for a model in response to changes in the workload. For this assignment, define a target-tracking scaling policy for a variant of your Endpoint and use the `SageMakerVariantInvocationsPerInstance` metric. `SageMakerVariantInvocationsPerInstance` is the average number of times per minute that the variant is invoked. Check [Automatically Scale Amazon SageMaker Models](https://docs.aws.amazon.com/sagemaker/latest/dg/endpoint-auto-scaling.html) for more information about auto scaling models.\n", + "\n", + "* Assignment 4.5 Modify the SageMaker Pipeline by adding a Lambda Step that will deploy the model directly as part of the pipeline. You won't need to set up Event Bridge anymore because your pipeline will automatically deploy the model.\n" + ] + }, + { + "cell_type": "markdown", + "id": "e544ae36-00b3-4bde-b133-c3a59bb7f1d8", + "metadata": {}, + "source": [ + "## Session 5 - Data Distribution Shifts And Model Monitoring\n", + "\n", + "In this session we'll set up a monitoring process to analyze the quality of the data our endpoint receives and the endpoint predictions. For this, we need to check the data received by the endpoint, generate ground truth labels, and compare them with a baseline performance.\n", + "\n", + " \"Monitoring\"\n", + "\n", + "To enable this functionality, we need a couple of steps:\n", + "\n", + "1. Create baselines we can use to compare against real-time traffic.\n", + "2. Set up a schedule to continuously evaluate and compare against the baselines.\n", + "\n", + "Check [Amazon SageMaker Model Monitor](https://sagemaker.readthedocs.io/en/stable/amazon_sagemaker_model_monitoring.html) for a brief explanation of how to use SageMaker's Model Monitoring functionality. [Monitor models for data and model quality, bias, and explainability](https://docs.aws.amazon.com/sagemaker/latest/dg/model-monitor.html) is a much more extensive guide to monitoring in Amazon SageMaker.\n" + ] + }, + { + "cell_type": "markdown", + "id": "0ef0ad20", + "metadata": {}, + "source": [ + "Let's start by defining three variables we'll use throughout the session:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 720, + "id": "2bb846d0", + "metadata": {}, + "outputs": [], + "source": [ + "GROUND_TRUTH_LOCATION = f\"{S3_LOCATION}/monitoring/groundtruth\"\n", + "DATA_QUALITY_LOCATION = f\"{S3_LOCATION}/monitoring/data-quality\"\n", + "MODEL_QUALITY_LOCATION = f\"{S3_LOCATION}/monitoring/model-quality\"" + ] + }, + { + "cell_type": "markdown", + "id": "24c26ac4-5d30-41e9-8952-e4deb39de819", + "metadata": {}, + "source": [ + "### Step 1 - Generating Data Quality Baseline\n", + "\n", + "Let's start by configuring a [Quality Check Step](https://docs.aws.amazon.com/sagemaker/latest/dg/build-and-manage-steps.html#step-type-quality-check) to compute the general statistics of the data we used to build our model.\n", + "\n", + "We can configure the instance that will run the quality check using the [CheckJobConfig](https://sagemaker.readthedocs.io/en/v2.73.0/workflows/pipelines/sagemaker.workflow.pipelines.html#sagemaker.workflow.check_job_config.CheckJobConfig) class, and we can use the `DataQualityCheckConfig` class to configure the job.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 721, + "id": "0b80bcab-d2c5-437c-a1c8-8eea208c0e29", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO:sagemaker.image_uris:Defaulting to the only supported framework/algorithm version: .\n", + "INFO:sagemaker.image_uris:Ignoring unnecessary instance type: None.\n" + ] + } + ], + "source": [ + "# | code: true\n", + "# | output: false\n", + "\n", + "from sagemaker.workflow.quality_check_step import (\n", + " QualityCheckStep,\n", + " DataQualityCheckConfig,\n", + ")\n", + "from sagemaker.workflow.check_job_config import CheckJobConfig\n", + "from sagemaker.model_monitor.dataset_format import DatasetFormat\n", + "\n", + "data_quality_baseline_step = QualityCheckStep(\n", + " name=\"generate-data-quality-baseline\",\n", + " check_job_config=CheckJobConfig(\n", + " instance_type=\"ml.c5.xlarge\",\n", + " instance_count=1,\n", + " volume_size_in_gb=20,\n", + " sagemaker_session=pipeline_session,\n", + " role=role,\n", + " ),\n", + " quality_check_config=DataQualityCheckConfig(\n", + " baseline_dataset=split_and_transform_data_step.properties.ProcessingOutputConfig.Outputs[\n", + " \"train-baseline\"\n", + " ].S3Output.S3Uri,\n", + " dataset_format=DatasetFormat.csv(header=True, output_columns_position=\"START\"),\n", + " output_s3_uri=DATA_QUALITY_LOCATION,\n", + " ),\n", + " skip_check=True,\n", + " register_new_baseline=True,\n", + " cache_config=cache_config,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "81430dfd-2524-43e4-bfe9-c6545316005d", + "metadata": { + "tags": [] + }, + "source": [ + "### Step 2 - Generating Test Predictions\n", + "\n", + "To create a baseline to compare the model performance, we must create predictions for the test set and compare the model's metrics with the model performance on production data. We can do this by running a [Batch Transform Job](https://docs.aws.amazon.com/sagemaker/latest/dg/batch-transform.html) to predict every sample from the test set. We can use a [Transform Step](https://docs.aws.amazon.com/sagemaker/latest/dg/build-and-manage-steps.html#step-type-transform) as part of the pipeline to run this job. This Batch Transform Job will run every sample from the training dataset through the model so we can compute the baseline metrics.\n", + "\n", + "The Transform Step requires a model to generate predictions, so we need a Model Step that creates a model:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 722, + "id": "8194b462", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/svpino/dev/ml.school/.venv/lib/python3.9/site-packages/sagemaker/workflow/pipeline_context.py:297: UserWarning: Running within a PipelineSession, there will be No Wait, No Logs, and No Job being started.\n", + " warnings.warn(\n" + ] + } + ], + "source": [ + "# | code: true\n", + "# | output: false\n", + "\n", + "from sagemaker.workflow.model_step import ModelStep\n", + "\n", + "create_model_step = ModelStep(\n", + " name=\"create\",\n", + " display_name=\"create-model\",\n", + " step_args=pipeline_model.create(instance_type=config[\"instance_type\"]),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "eddb6ac7", + "metadata": {}, + "source": [ + "Let's configure the Batch Transform Job using an instance of the [Transformer](https://sagemaker.readthedocs.io/en/stable/api/inference/transformer.html) class:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 723, + "id": "bf6aa4f0", + "metadata": {}, + "outputs": [], + "source": [ + "from sagemaker.transformer import Transformer\n", + "\n", + "transformer = Transformer(\n", + " model_name=create_model_step.properties.ModelName,\n", + " instance_type=config[\"instance_type\"],\n", + " instance_count=1,\n", + " strategy=\"MultiRecord\",\n", + " accept=\"text/csv\",\n", + " assemble_with=\"Line\",\n", + " output_path=f\"{S3_LOCATION}/transform\",\n", + " sagemaker_session=pipeline_session,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "a7f01fb9", + "metadata": {}, + "source": [ + "We can now set up the [Transform Step](https://docs.aws.amazon.com/sagemaker/latest/dg/build-and-manage-steps.html#step-type-transform) using the transformer we configured before.\n", + "\n", + "Notice the following:\n", + "\n", + "- We'll generate predictions for the baseline output that we generated when we split and transformed the data. This baseline is the same data we used to test the model, but we saved it in its original format before transforming it.\n", + "- The output of this Batch Transform Job will have two fields. The first one will be the ground truth label, and the second one will be the prediction of the model.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 724, + "id": "1987a788-de7a-4f60-ac8d-819d9ffcdf8e", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# | code: true\n", + "# | output: false\n", + "\n", + "from sagemaker.workflow.steps import TransformStep\n", + "\n", + "generate_test_predictions_step = TransformStep(\n", + " name=\"generate-test-predictions\",\n", + " step_args=transformer.transform(\n", + " # We will use the baseline set we generated when we split the data.\n", + " # This set corresponds to the test split before the transformation step.\n", + " data=split_and_transform_data_step.properties.ProcessingOutputConfig.Outputs[\n", + " \"test-baseline\"\n", + " ].S3Output.S3Uri,\n", + "\n", + " join_source=\"Input\",\n", + " split_type=\"Line\",\n", + " content_type=\"text/csv\",\n", + " \n", + " # We want to output the first and the last field from the joint set.\n", + " # The first field corresponds to the groundtruth, and the last field\n", + " # corresponds to the prediction.\n", + " output_filter=\"$[0,-1]\",\n", + " ),\n", + " cache_config=cache_config,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "2fafc7c4-6fef-4832-8b99-8c45d078fdd2", + "metadata": {}, + "source": [ + "### Step 3 - Generating Model Quality Baseline\n", + "\n", + "Let's now configure the [Quality Check Step](https://docs.aws.amazon.com/sagemaker/latest/dg/build-and-manage-steps.html#step-type-quality-check) and feed it the data we generated in the Transform Step. This step will automatically compute the performance metrics of the model on the test set:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 725, + "id": "9aa3a284-8763-4000-a263-70314b530652", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO:sagemaker.image_uris:Defaulting to the only supported framework/algorithm version: .\n", + "INFO:sagemaker.image_uris:Ignoring unnecessary instance type: None.\n" + ] + } + ], + "source": [ + "# | code: true\n", + "# | output: false\n", + "\n", + "from sagemaker.workflow.quality_check_step import ModelQualityCheckConfig\n", + "\n", + "model_quality_baseline_step = QualityCheckStep(\n", + " name=\"generate-model-quality-baseline\",\n", + " check_job_config=CheckJobConfig(\n", + " instance_type=\"ml.c5.xlarge\",\n", + " instance_count=1,\n", + " volume_size_in_gb=20,\n", + " sagemaker_session=pipeline_session,\n", + " role=role,\n", + " ),\n", + " quality_check_config=ModelQualityCheckConfig(\n", + " # We are going to use the output of the Transform Step to generate\n", + " # the model quality baseline.\n", + " baseline_dataset=generate_test_predictions_step.properties.TransformOutput.S3OutputPath,\n", + " dataset_format=DatasetFormat.csv(header=False),\n", + "\n", + " # We need to specify the problem type and the fields where the prediction\n", + " # and groundtruth are so the process knows how to interpret the results.\n", + " problem_type=\"MulticlassClassification\",\n", + " \n", + " # Since the data doesn't have headers, SageMaker will autocreate headers for it.\n", + " # _c0 corresponds to the first column, and _c1 corresponds to the second column.\n", + " ground_truth_attribute=\"_c0\",\n", + " inference_attribute=\"_c1\",\n", + " output_s3_uri=MODEL_QUALITY_LOCATION,\n", + " ),\n", + " skip_check=True,\n", + " register_new_baseline=True,\n", + " cache_config=cache_config,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "693535ba-fca7-4e89-a4cb-b4f333fa2d03", + "metadata": {}, + "source": [ + "### Step 4 - Setting up Model Metrics\n", + "\n", + "We can configure a new set of [ModelMetrics](https://sagemaker.readthedocs.io/en/stable/api/inference/model_monitor.html#sagemaker.model_metrics.ModelMetrics) using the results of the Data and Model Quality Steps. Check [Baseline and model version lifecycle and evolution with SageMaker Pipelines](https://docs.aws.amazon.com/sagemaker/latest/dg/pipelines-quality-clarify-baseline-lifecycle.html#pipelines-quality-clarify-baseline-evolution) for an explanation of how SageMaker uses the `DriftCheckBaselines`.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 726, + "id": "a773f134-ac2f-4dba-976e-9b7f0b384b6e", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "from sagemaker.drift_check_baselines import DriftCheckBaselines\n", + "\n", + "model_metrics = ModelMetrics(\n", + " model_data_statistics=MetricsSource(\n", + " s3_uri=data_quality_baseline_step.properties.CalculatedBaselineStatistics,\n", + " content_type=\"application/json\",\n", + " ),\n", + " model_data_constraints=MetricsSource(\n", + " s3_uri=data_quality_baseline_step.properties.CalculatedBaselineConstraints,\n", + " content_type=\"application/json\",\n", + " ),\n", + " model_statistics=MetricsSource(\n", + " s3_uri=model_quality_baseline_step.properties.CalculatedBaselineStatistics,\n", + " content_type=\"application/json\",\n", + " ),\n", + " model_constraints=MetricsSource(\n", + " s3_uri=model_quality_baseline_step.properties.CalculatedBaselineConstraints,\n", + " content_type=\"application/json\",\n", + " ),\n", + ")\n", + "\n", + "drift_check_baselines = DriftCheckBaselines(\n", + " model_data_statistics=MetricsSource(\n", + " s3_uri=data_quality_baseline_step.properties.BaselineUsedForDriftCheckStatistics,\n", + " content_type=\"application/json\",\n", + " ),\n", + " model_data_constraints=MetricsSource(\n", + " s3_uri=data_quality_baseline_step.properties.BaselineUsedForDriftCheckConstraints,\n", + " content_type=\"application/json\",\n", + " ),\n", + " model_statistics=MetricsSource(\n", + " s3_uri=model_quality_baseline_step.properties.BaselineUsedForDriftCheckStatistics,\n", + " content_type=\"application/json\",\n", + " ),\n", + " model_constraints=MetricsSource(\n", + " s3_uri=model_quality_baseline_step.properties.BaselineUsedForDriftCheckConstraints,\n", + " content_type=\"application/json\",\n", + " ),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "ba3487a0-05ad-4f3a-8f50-9884dc2aef64", + "metadata": {}, + "source": [ + "### Step 5 - Modifying the Registration Step\n", + "\n", + "Since we want to register the model using the new metrics, we need to modify the Registration Step to use the new metrics:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 727, + "id": "7056a009-91c0-4955-90dd-b90ef8cab149", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# | code: true\n", + "# | output: false\n", + "\n", + "register_model_step = ModelStep(\n", + " name=\"register\",\n", + " display_name=\"register-model\",\n", + " step_args=pipeline_model.register(\n", + " model_package_group_name=PIPELINE_MODEL_PACKAGE_GROUP,\n", + " model_metrics=model_metrics,\n", + " drift_check_baselines=drift_check_baselines,\n", + " approval_status=\"PendingManualApproval\",\n", + " content_types=[\"text/csv\", \"application/json\"],\n", + " response_types=[\"text/csv\", \"application/json\"],\n", + " inference_instances=[\"ml.m5.xlarge\"],\n", + " transform_instances=[\"ml.m5.xlarge\"],\n", + " domain=\"MACHINE_LEARNING\",\n", + " task=\"CLASSIFICATION\",\n", + " framework=\"TENSORFLOW\",\n", + " framework_version=config[\"framework_version\"],\n", + " ),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "0d00b5e6-9858-4acc-bbfe-a2ce24ec20e0", + "metadata": {}, + "source": [ + "### Step 6 - Modifying the Condition Step\n", + "\n", + "Since we modified the registration step and added a few more steps, we need to modify the Condition Step. Now, we want to generate the test predictions and compute the model quality baseline if the condition is successful:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 728, + "id": "bacaa9c6-22b0-48df-b138-95b6422fe834", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "condition_step = ConditionStep(\n", + " name=\"check-model-accuracy\",\n", + " conditions=[condition],\n", + " if_steps=[\n", + " create_model_step,\n", + " generate_test_predictions_step,\n", + " model_quality_baseline_step,\n", + " register_model_step,\n", + " ]\n", + " if not LOCAL_MODE\n", + " else [],\n", + " else_steps=[fail_step],\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "c95a7905-2550-4979-b885-f2daabb5d45e", + "metadata": {}, + "source": [ + "### Step 7 - Creating the Pipeline\n", + "\n", + "We can now define the SageMaker Pipeline and submit its definition to the SageMaker Pipelines service to create the pipeline if it doesn't exist or update it if it does.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 729, + "id": "4da5e453-acd8-47a0-a39f-264d05dd93d0", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Using provided s3_resource\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO:sagemaker.image_uris:image_uri is not presented, retrieving image_uri based on instance_type, framework etc.\n", + "INFO:sagemaker.processing:Uploaded None to s3://mlschool/session5-pipeline/code/09fea667a5ab7c37a068f22c00762d0b/sourcedir.tar.gz\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Using provided s3_resource\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO:sagemaker.processing:runproc.sh uploaded to s3://mlschool/session5-pipeline/code/2c207c809cb0e0e9a1d77e5247f961f9/runproc.sh\n", + "WARNING:sagemaker.workflow._utils:Popping out 'CertifyForMarketplace' from the pipeline definition since it will be overridden in pipeline execution time.\n", + "INFO:sagemaker.image_uris:image_uri is not presented, retrieving image_uri based on instance_type, framework etc.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Using provided s3_resource\n", + "Using provided s3_resource\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO:sagemaker.processing:Uploaded None to s3://mlschool/session5-pipeline/code/09fea667a5ab7c37a068f22c00762d0b/sourcedir.tar.gz\n", + "INFO:sagemaker.processing:runproc.sh uploaded to s3://mlschool/session5-pipeline/code/2c207c809cb0e0e9a1d77e5247f961f9/runproc.sh\n" + ] + }, + { + "data": { + "text/plain": [ + "{'PipelineArn': 'arn:aws:sagemaker:us-east-1:325223348818:pipeline/session5-pipeline',\n", + " 'ResponseMetadata': {'RequestId': 'e104a5af-2148-4ab4-85b3-af898d3bd315',\n", + " 'HTTPStatusCode': 200,\n", + " 'HTTPHeaders': {'x-amzn-requestid': 'e104a5af-2148-4ab4-85b3-af898d3bd315',\n", + " 'content-type': 'application/x-amz-json-1.1',\n", + " 'content-length': '85',\n", + " 'date': 'Fri, 27 Oct 2023 14:38:52 GMT'},\n", + " 'RetryAttempts': 0}}" + ] + }, + "execution_count": 729, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# | code: true\n", + "# | output: false\n", + "\n", + "session5_pipeline = Pipeline(\n", + " name=\"session5-pipeline\",\n", + " parameters=[dataset_location, accuracy_threshold],\n", + " steps=[\n", + " split_and_transform_data_step,\n", + " tune_model_step if USE_TUNING_STEP else train_model_step,\n", + " evaluate_model_step,\n", + " data_quality_baseline_step,\n", + " condition_step,\n", + " ],\n", + " pipeline_definition_config=pipeline_definition_config,\n", + " sagemaker_session=config[\"session\"],\n", + ")\n", + "\n", + "session5_pipeline.upsert(role_arn=role)" + ] + }, + { + "cell_type": "markdown", + "id": "9e6b1b39", + "metadata": {}, + "source": [ + "We can now start the pipeline:\n" + ] + }, + { + "cell_type": "markdown", + "id": "9d6e5995", + "metadata": {}, + "source": [ + "#| hide\n", + "\n", + "
Note: \n", + " The %%script cell magic is a convenient way to prevent the notebook from executing a specific cell. If you want to run the cell, comment out the line containing the %%script cell magic.\n", + "
\n" + ] + }, + { + "cell_type": "code", + "execution_count": 739, + "id": "10ba9909", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "_PipelineExecution(arn='arn:aws:sagemaker:us-east-1:325223348818:pipeline/session5-pipeline/execution/ifgn9itt6qcy', sagemaker_session=)" + ] + }, + "execution_count": 739, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# %%script false --no-raise-error\n", + "\n", + "#| eval: false\n", + "#| code: true\n", + "#| output: false\n", + "\n", + "session5_pipeline.start()" + ] + }, + { + "cell_type": "markdown", + "id": "6fd182a9", + "metadata": {}, + "source": [ + "### Step 8 - Checking Constraints and Statistics\n", + "\n", + "Our pipeline generated data baseline statistics and constraints. We can take a look at what these values look like by downloading them from S3. You need to wait for the pipeline to finish running before these files are available.\n", + "\n", + "Here are the data quality statistics:" + ] + }, + { + "cell_type": "code", + "execution_count": 752, + "id": "42daa82b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{\n", + " \"name\": \"island\",\n", + " \"inferred_type\": \"String\",\n", + " \"string_statistics\": {\n", + " \"common\": {\n", + " \"num_present\": 232,\n", + " \"num_missing\": 0\n", + " },\n", + " \"distinct_count\": 3.0,\n", + " \"distribution\": {\n", + " \"categorical\": {\n", + " \"buckets\": [\n", + " {\n", + " \"value\": \"Dream\",\n", + " \"count\": 89\n", + " },\n", + " {\n", + " \"value\": \"Torgersen\",\n", + " \"count\": 24\n", + " },\n", + " {\n", + " \"value\": \"Biscoe\",\n", + " \"count\": 119\n", + " }\n", + " ]\n", + " }\n", + " }\n", + " }\n", + "}\n" + ] + } + ], + "source": [ + "from sagemaker.s3 import S3Downloader\n", + "\n", + "try:\n", + " response = json.loads(\n", + " S3Downloader.read_file(f\"{DATA_QUALITY_LOCATION}/statistics.json\")\n", + " )\n", + " print(json.dumps(response[\"features\"][1], indent=2))\n", + "except Exception as e:\n", + " pass" + ] + }, + { + "cell_type": "markdown", + "id": "8104ad3c", + "metadata": {}, + "source": [ + "Here are the data quality constraints:" + ] + }, + { + "cell_type": "code", + "execution_count": 753, + "id": "898d9626", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{\n", + " \"name\": \"island\",\n", + " \"inferred_type\": \"String\",\n", + " \"completeness\": 1.0,\n", + " \"string_constraints\": {\n", + " \"domains\": [\n", + " \"Dream\",\n", + " \"Torgersen\",\n", + " \"Biscoe\"\n", + " ]\n", + " }\n", + "}\n" + ] + } + ], + "source": [ + "try:\n", + " response = json.loads(S3Downloader.read_file(f\"{DATA_QUALITY_LOCATION}/constraints.json\"))\n", + " print(json.dumps(response[\"features\"][1], indent=2))\n", + "except Exception as e:\n", + " pass" + ] + }, + { + "cell_type": "markdown", + "id": "35eaf9af", + "metadata": {}, + "source": [ + "And here are the model quality constraints:" + ] + }, + { + "cell_type": "code", + "execution_count": 754, + "id": "2df52332", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{\n", + " \"version\": 0.0,\n", + " \"multiclass_classification_constraints\": {\n", + " \"accuracy\": {\n", + " \"threshold\": 0.9259259259259259,\n", + " \"comparison_operator\": \"LessThanThreshold\"\n", + " },\n", + " \"weighted_recall\": {\n", + " \"threshold\": 0.9259259259259259,\n", + " \"comparison_operator\": \"LessThanThreshold\"\n", + " },\n", + " \"weighted_precision\": {\n", + " \"threshold\": 0.933862433862434,\n", + " \"comparison_operator\": \"LessThanThreshold\"\n", + " },\n", + " \"weighted_f0_5\": {\n", + " \"threshold\": 0.928855833521148,\n", + " \"comparison_operator\": \"LessThanThreshold\"\n", + " },\n", + " \"weighted_f1\": {\n", + " \"threshold\": 0.9247293447293448,\n", + " \"comparison_operator\": \"LessThanThreshold\"\n", + " },\n", + " \"weighted_f2\": {\n", + " \"threshold\": 0.9242942991137502,\n", + " \"comparison_operator\": \"LessThanThreshold\"\n", + " }\n", + " }\n", + "}\n" + ] + } + ], + "source": [ + "try:\n", + " response = json.loads(S3Downloader.read_file(f\"{MODEL_QUALITY_LOCATION}/constraints.json\"))\n", + " print(json.dumps(response, indent=2))\n", + "except Exception as e:\n", + " pass" + ] + }, + { + "cell_type": "markdown", + "id": "b948aa92-8064-4f03-af08-0f6a8fc329cf", + "metadata": {}, + "source": [ + "### Step 9 - Generating Fake Traffic\n", + "\n", + "To test the monitoring functionality, we need to generate traffic to the endpoint. To generate traffic, we will send every sample from the dataset to the endpoint to simulate real prediction requests:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 755, + "id": "c658bad0", + "metadata": {}, + "outputs": [], + "source": [ + "# | code: true\n", + "# | output: false\n", + "\n", + "from sagemaker.serializers import JSONSerializer\n", + "\n", + "data = penguins.drop([\"species\"], axis=1)\n", + "data = data.dropna()\n", + "\n", + "predictor = Predictor(\n", + " endpoint_name=ENDPOINT,\n", + " serializer=JSONSerializer(),\n", + " sagemaker_session=sagemaker_session,\n", + ")\n", + "\n", + "for index, row in data.iterrows():\n", + " try:\n", + " predictor.predict(row.to_dict(), inference_id=str(index))\n", + " except Exception as e:\n", + " print(e)\n", + " break" + ] + }, + { + "cell_type": "markdown", + "id": "0d3f61b9", + "metadata": {}, + "source": [ + "We can check the location where the endpoint stores the captured data, download a file, and display its content. It may take a few minutes for the first few files to show up in S3.\n", + "\n", + "These files contain the data captured by the endpoint in a SageMaker-specific JSON-line format. Each inference request is captured in a single line in the `jsonl` file. The line contains both the input and output merged together:" + ] + }, + { + "cell_type": "code", + "execution_count": 756, + "id": "3f35e8db-24d7-4d4b-9264-78ee5070cf27", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{\n", + " \"captureData\": {\n", + " \"endpointInput\": {\n", + " \"observedContentType\": \"application/json\",\n", + " \"mode\": \"INPUT\",\n", + " \"data\": \"{\\\"island\\\": \\\"Torgersen\\\", \\\"culmen_length_mm\\\": 39.1, \\\"culmen_depth_mm\\\": 18.7, \\\"flipper_length_mm\\\": 181.0, \\\"body_mass_g\\\": 3750.0, \\\"sex\\\": \\\"MALE\\\"}\",\n", + " \"encoding\": \"JSON\"\n", + " },\n", + " \"endpointOutput\": {\n", + " \"observedContentType\": \"application/json\",\n", + " \"mode\": \"OUTPUT\",\n", + " \"data\": \"{\\\"prediction\\\": \\\"Adelie\\\", \\\"confidence\\\": 0.953110516}\",\n", + " \"encoding\": \"JSON\"\n", + " }\n", + " },\n", + " \"eventMetadata\": {\n", + " \"eventId\": \"ddf80c99-e582-4243-9309-4bc9085c01ec\",\n", + " \"inferenceId\": \"0\",\n", + " \"inferenceTime\": \"2023-10-24T19:10:30Z\"\n", + " },\n", + " \"eventVersion\": \"0\"\n", + "}\n" + ] + } + ], + "source": [ + "files = S3Downloader.list(DATA_CAPTURE_DESTINATION)[:3]\n", + "if len(files):\n", + " lines = S3Downloader.read_file(files[0])\n", + " print(json.dumps(json.loads(lines.split(\"\\n\")[0]), indent=2))" + ] + }, + { + "cell_type": "markdown", + "id": "59e53138", + "metadata": {}, + "source": [ + "These files contain the data captured by the endpoint in a SageMaker-specific JSON-line format. Each inference request is captured in a single line in the `jsonl` file. The line contains both the input and output merged together:" + ] + }, + { + "cell_type": "markdown", + "id": "5754a314-3bc0-4b41-8767-e9f06d96d250", + "metadata": {}, + "source": [ + "### Step 10 - Generating Fake Labels\n", + "\n", + "To test the performance of the model, we need to label the samples captured by the endpoint. We can simulate the labeling process by generating a random label for every sample. Check [Ingest Ground Truth Labels and Merge Them With Predictions](https://docs.aws.amazon.com/sagemaker/latest/dg/model-monitor-model-quality-merge.html) for more information about this.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 757, + "id": "bb999995", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'s3://mlschool/penguins/monitoring/groundtruth/2023/10/27/17/0816.jsonl'" + ] + }, + "execution_count": 757, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "#| code: true\n", + "#| output: false\n", + "\n", + "import random\n", + "from datetime import datetime\n", + "from sagemaker.s3 import S3Uploader\n", + "\n", + "records = []\n", + "for inference_id in range(len(data)):\n", + " random.seed(inference_id)\n", + "\n", + " records.append(json.dumps({\n", + " \"groundTruthData\": {\n", + " \"data\": random.choice([\"Adelie\", \"Chinstrap\", \"Gentoo\"]),\n", + " \"encoding\": \"CSV\",\n", + " },\n", + " \"eventMetadata\": {\n", + " \"eventId\": str(inference_id),\n", + " },\n", + " \"eventVersion\": \"0\",\n", + " }))\n", + "\n", + "groundtruth_payload = \"\\n\".join(records)\n", + "upload_time = datetime.utcnow()\n", + "uri = f\"{GROUND_TRUTH_LOCATION}/{upload_time:%Y/%m/%d/%H/%M%S}.jsonl\"\n", + "S3Uploader.upload_string_as_file_body(groundtruth_payload, uri)" + ] + }, + { + "cell_type": "markdown", + "id": "a65bd669", + "metadata": {}, + "source": [ + "### Step 11 - Preparing Monitoring Functions\n", + "\n", + "Let's create a few functions that will help us work with monitoring schedules later on:" + ] + }, + { + "cell_type": "code", + "execution_count": 758, + "id": "da145ba1-4966-4dab-8a73-281db364cbc7", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "from sagemaker.model_monitor import MonitoringExecution\n", + "\n", + "\n", + "def describe_monitoring_schedules(endpoint_name):\n", + " schedules = []\n", + " response = sagemaker_client.list_monitoring_schedules(EndpointName=endpoint_name)[\n", + " \"MonitoringScheduleSummaries\"\n", + " ]\n", + " for item in response:\n", + " name = item[\"MonitoringScheduleName\"]\n", + " schedule = {\n", + " \"MonitoringScheduleName\": name,\n", + " \"MonitoringType\": item[\"MonitoringType\"],\n", + " }\n", + "\n", + " description = sagemaker_client.describe_monitoring_schedule(\n", + " MonitoringScheduleName=name\n", + " )\n", + "\n", + " schedule[\"Status\"] = description[\"LastMonitoringExecutionSummary\"][\n", + " \"MonitoringExecutionStatus\"\n", + " ]\n", + "\n", + " if schedule[\"Status\"] == \"Failed\":\n", + " schedule[\"FailureReason\"] = description[\"LastMonitoringExecutionSummary\"][\n", + " \"FailureReason\"\n", + " ]\n", + " elif schedule[\"Status\"] == \"CompletedWithViolations\":\n", + " processing_job_arn = description[\"LastMonitoringExecutionSummary\"][\n", + " \"ProcessingJobArn\"\n", + " ]\n", + " execution = MonitoringExecution.from_processing_arn(\n", + " sagemaker_session=sagemaker_session,\n", + " processing_job_arn=processing_job_arn,\n", + " )\n", + " execution_destination = execution.output.destination\n", + "\n", + " violations_filepath = os.path.join(\n", + " execution_destination, \"constraint_violations.json\"\n", + " )\n", + " violations = json.loads(S3Downloader.read_file(violations_filepath))[\n", + " \"violations\"\n", + " ]\n", + "\n", + " schedule[\"Violations\"] = violations\n", + "\n", + " schedules.append(schedule)\n", + "\n", + " return schedules\n", + "\n", + "\n", + "def describe_monitoring_schedule(endpoint_name, monitoring_type):\n", + " found = False\n", + "\n", + " schedules = describe_monitoring_schedules(endpoint_name)\n", + " for schedule in schedules:\n", + " if schedule[\"MonitoringType\"] == monitoring_type:\n", + " found = True\n", + " print(json.dumps(schedule, indent=2))\n", + "\n", + " if not found:\n", + " print(f\"There's no {monitoring_type} Monitoring Schedule.\")\n", + "\n", + "\n", + "def describe_data_monitoring_schedule(endpoint_name):\n", + " describe_monitoring_schedule(endpoint_name, \"DataQuality\")\n", + "\n", + "\n", + "def describe_model_monitoring_schedule(endpoint_name):\n", + " describe_monitoring_schedule(endpoint_name, \"ModelQuality\")\n", + "\n", + "\n", + "def delete_monitoring_schedule(endpoint_name, monitoring_type):\n", + " attempts = 30\n", + " found = False\n", + "\n", + " response = sagemaker_client.list_monitoring_schedules(EndpointName=endpoint_name)[\n", + " \"MonitoringScheduleSummaries\"\n", + " ]\n", + " for item in response:\n", + " if item[\"MonitoringType\"] == monitoring_type:\n", + " found = True\n", + " status = sagemaker_client.describe_monitoring_schedule(\n", + " MonitoringScheduleName=item[\"MonitoringScheduleName\"]\n", + " )[\"MonitoringScheduleStatus\"]\n", + " while status in (\"Pending\", \"InProgress\") and attempts > 0:\n", + " attempts -= 1\n", + " print(\n", + " f\"Monitoring schedule status: {status}. Waiting for it to finish.\"\n", + " )\n", + " sleep(30)\n", + "\n", + " status = sagemaker_client.describe_monitoring_schedule(\n", + " MonitoringScheduleName=item[\"MonitoringScheduleName\"]\n", + " )[\"MonitoringScheduleStatus\"]\n", + "\n", + " if status not in (\"Pending\", \"InProgress\"):\n", + " sagemaker_client.delete_monitoring_schedule(\n", + " MonitoringScheduleName=item[\"MonitoringScheduleName\"]\n", + " )\n", + " print(\"Monitoring schedule deleted.\")\n", + " else:\n", + " print(\"Waiting for monitoring schedule timed out\")\n", + "\n", + " if not found:\n", + " print(f\"There's no {monitoring_type} Monitoring Schedule.\")\n", + "\n", + "\n", + "def delete_data_monitoring_schedule(endpoint_name):\n", + " delete_monitoring_schedule(endpoint_name, \"DataQuality\")\n", + "\n", + "\n", + "def delete_model_monitoring_schedule(endpoint_name):\n", + " delete_monitoring_schedule(endpoint_name, \"ModelQuality\")" + ] + }, + { + "cell_type": "markdown", + "id": "d936df76-e0b8-4dad-a04f-ef77ce2a2df1", + "metadata": {}, + "source": [ + "### Step 12 - Setting Up Data Monitoring Job\n", + "\n", + "SageMaker looks for violations in the data captured by the endpoint. By default, it combines the input data with the endpoint output and compares the result with the baseline we generated. If we let SageMaker do this, we will get a few violations, for example an \"extra column check\" violation because the field `confidence` doesn't exist in the baseline data.\n", + "\n", + "We can fix these violations by creating a preprocessing script configuring the data we want the monitoring job to use. Check [Preprocessing and Postprocessing](https://docs.aws.amazon.com/sagemaker/latest/dg/model-monitor-pre-and-post-processing.html) for more information about how to configure these scripts.\n", + "\n", + "Let's define the name of the preprocessing script:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 759, + "id": "cc119422-2e85-4e8c-86cd-6d59e353d09d", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "DATA_QUALITY_PREPROCESSOR = \"data_quality_preprocessor.py\"" + ] + }, + { + "cell_type": "markdown", + "id": "72c1023e", + "metadata": {}, + "source": [ + "We can now define the preprocessing script. Notice that this script will return the input data the endpoint receives with a new `species` column containing the prediction of the model:" + ] + }, + { + "cell_type": "code", + "execution_count": 760, + "id": "083b0bd0-4035-43fe-9b2c-946b12a5e266", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Overwriting code/data_quality_preprocessor.py\n" + ] + } + ], + "source": [ + "%%writefile {CODE_FOLDER}/{DATA_QUALITY_PREPROCESSOR}\n", + "#| code: true\n", + "#| output: false\n", + "\n", + "import json\n", + "\n", + "def preprocess_handler(inference_record):\n", + " input_data = inference_record.endpoint_input.data\n", + " output_data = json.loads(inference_record.endpoint_output.data)\n", + "\n", + " response = json.loads(input_data)\n", + " response[\"species\"] = output_data[\"prediction\"]\n", + "\n", + " # The `response` variable contains the data that we want the\n", + " # monitoring job to use to compare with the baseline.\n", + " return response" + ] + }, + { + "cell_type": "markdown", + "id": "840d54c5-f09c-4559-a1d2-63587da0ad14", + "metadata": {}, + "source": [ + "The monitoring schedule expects an S3 location pointing to the preprocessing script. Let's upload the script to the default bucket.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 761, + "id": "96e5c0c1-7e40-47df-8f40-1d891db13875", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO:botocore.credentials:Found credentials in shared credentials file: ~/.aws/credentials\n" + ] + } + ], + "source": [ + "#| code: true\n", + "#| output: false\n", + "\n", + "bucket = boto3.Session().resource(\"s3\").Bucket(pipeline_session.default_bucket())\n", + "prefix = \"penguins-monitoring\"\n", + "bucket.Object(os.path.join(prefix, DATA_QUALITY_PREPROCESSOR)).upload_file(\n", + " str(CODE_FOLDER / DATA_QUALITY_PREPROCESSOR)\n", + ")\n", + "data_quality_preprocessor = (\n", + " f\"s3://{os.path.join(bucket.name, prefix, DATA_QUALITY_PREPROCESSOR)}\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "56e107eb-546d-431c-b74d-1bfd412711b7", + "metadata": {}, + "source": [ + "We can now set up the Data Quality Monitoring Job using the [DefaultModelMonitor](https://sagemaker.readthedocs.io/en/stable/api/inference/model_monitor.html#sagemaker.model_monitor.model_monitoring.DefaultModelMonitor) class. Notice how we specify the `record_preprocessor_script` using the S3 location where we uploaded our script." + ] + }, + { + "cell_type": "markdown", + "id": "e653b628", + "metadata": {}, + "source": [ + "#| hide\n", + "\n", + "
Note: \n", + " The %%script cell magic is a convenient way to prevent the notebook from executing a specific cell. If you want to run the cell, comment out the line containing the %%script cell magic.\n", + "
" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15caf9e1-97fc-4379-893b-6062d4bd876e", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# %%script false --no-raise-error\n", + "#| code: true\n", + "#| output: false\n", + "#| eval: false\n", + "\n", + "from sagemaker.model_monitor import CronExpressionGenerator, DefaultModelMonitor\n", + "\n", + "data_monitor = DefaultModelMonitor(\n", + " instance_type=\"ml.m5.xlarge\",\n", + " instance_count=1,\n", + " max_runtime_in_seconds=3600,\n", + " role=role,\n", + ")\n", + "\n", + "data_monitor.create_monitoring_schedule(\n", + " monitor_schedule_name=\"penguins-data-monitoring-schedule\",\n", + " endpoint_input=ENDPOINT,\n", + " record_preprocessor_script=data_quality_preprocessor,\n", + " statistics=f\"{DATA_QUALITY_LOCATION}/statistics.json\",\n", + " constraints=f\"{DATA_QUALITY_LOCATION}/constraints.json\",\n", + " schedule_cron_expression=CronExpressionGenerator.hourly(),\n", + " output_s3_uri=DATA_QUALITY_LOCATION,\n", + " enable_cloudwatch_metrics=True,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "018800f7-315f-4f5e-b082-ba94bbde91ad", + "metadata": {}, + "source": [ + "We can check the results of the monitoring job by looking at whether it generated any violations:" + ] + }, + { + "cell_type": "code", + "execution_count": 781, + "id": "2c04fdd4-cc03-496c-a0a1-405854505c46", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{\n", + " \"MonitoringScheduleName\": \"penguins-data-monitoring-schedule\",\n", + " \"MonitoringType\": \"DataQuality\",\n", + " \"Status\": \"Failed\",\n", + " \"FailureReason\": \"Job inputs had no data\"\n", + "}\n" + ] + } + ], + "source": [ + "describe_data_monitoring_schedule(ENDPOINT)" + ] + }, + { + "cell_type": "markdown", + "id": "3a9d201d-f60f-49f2-b4e9-eb0a0159ecfd", + "metadata": {}, + "source": [ + "### Step 13 - Setting up Model Monitoring Job\n", + "\n", + "To set up a Model Quality Monitoring Job, we can use the [ModelQualityMonitor](https://sagemaker.readthedocs.io/en/stable/api/inference/model_monitor.html#sagemaker.model_monitor.model_monitoring.ModelQualityMonitor) class. The [EndpointInput](https://sagemaker.readthedocs.io/en/v2.24.2/api/inference/model_monitor.html#sagemaker.model_monitor.model_monitoring.EndpointInput) instance configures the attribute the monitoring job should use to determine the prediction from the model.\n", + "\n", + "Check [Amazon SageMaker Model Quality Monitor](https://sagemaker-examples.readthedocs.io/en/latest/sagemaker_model_monitor/model_quality/model_quality_churn_sdk.html) for a complete tutorial on how to run a Model Monitoring Job in SageMaker." + ] + }, + { + "cell_type": "markdown", + "id": "9d217afd", + "metadata": {}, + "source": [ + "We can now start the Model Quality Monitoring Job:" + ] + }, + { + "cell_type": "markdown", + "id": "cd771884", + "metadata": {}, + "source": [ + "#| hide\n", + "\n", + "
Note: \n", + " The %%script cell magic is a convenient way to prevent the notebook from executing a specific cell. If you want to run the cell, comment out the line containing the %%script cell magic.\n", + "
" + ] + }, + { + "cell_type": "code", + "execution_count": 388, + "id": "070e0d73-5375-4fc3-b94c-da0574600c05", + "metadata": {}, + "outputs": [], + "source": [ + "%%script false --no-raise-error\n", + "#| code: true\n", + "#| output: false\n", + "#| eval: false\n", + "from sagemaker.model_monitor import ModelQualityMonitor, EndpointInput\n", + "\n", + "model_monitor = ModelQualityMonitor(\n", + " instance_type=\"ml.m5.xlarge\",\n", + " instance_count=1,\n", + " max_runtime_in_seconds=1800,\n", + " role=role\n", + ")\n", + "\n", + "model_monitor.create_monitoring_schedule(\n", + " monitor_schedule_name=\"penguins-model-monitoring-schedule\",\n", + " \n", + " endpoint_input = EndpointInput(\n", + " endpoint_name=ENDPOINT,\n", + " inference_attribute=\"prediction\",\n", + " destination=\"/opt/ml/processing/input_data\",\n", + " ),\n", + " \n", + " problem_type=\"MulticlassClassification\",\n", + " ground_truth_input=GROUND_TRUTH_LOCATION,\n", + " constraints=f\"{MODEL_QUALITY_LOCATION}/constraints.json\",\n", + " schedule_cron_expression=CronExpressionGenerator.hourly(),\n", + " output_s3_uri=MODEL_QUALITY_LOCATION,\n", + " enable_cloudwatch_metrics=True,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "8d9e523e-49c5-4382-b28a-cdbece9bd0e0", + "metadata": {}, + "source": [ + "We can check the results of the monitoring job by looking at whether it generated any violations.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 402, + "id": "347de298-16f2-42e0-85c4-dfc916080020", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{\n", + " \"MonitoringScheduleName\": \"penguins-model-monitoring-schedule\",\n", + " \"MonitoringType\": \"ModelQuality\",\n", + " \"Status\": \"CompletedWithViolations\",\n", + " \"Violations\": [\n", + " {\n", + " \"constraint_check_type\": \"LessThanThreshold\",\n", + " \"description\": \"Metric weightedF2 with 0.3505018546481581 +/- 0.004778110439777429 was LessThanThreshold '0.9242942991137502'\",\n", + " \"metric_name\": \"weightedF2\"\n", + " },\n", + " {\n", + " \"constraint_check_type\": \"LessThanThreshold\",\n", + " \"description\": \"Metric accuracy with 0.35755813953488375 +/- 0.004625699974871179 was LessThanThreshold '0.9259259259259259'\",\n", + " \"metric_name\": \"accuracy\"\n", + " },\n", + " {\n", + " \"constraint_check_type\": \"LessThanThreshold\",\n", + " \"description\": \"Metric weightedRecall with 0.3575581395348837 +/- 0.004625699974871179 was LessThanThreshold '0.9259259259259259'\",\n", + " \"metric_name\": \"weightedRecall\"\n", + " },\n", + " {\n", + " \"constraint_check_type\": \"LessThanThreshold\",\n", + " \"description\": \"Metric weightedPrecision with 0.35662633279042494 +/- 0.005592963346101618 was LessThanThreshold '0.933862433862434'\",\n", + " \"metric_name\": \"weightedPrecision\"\n", + " },\n", + " {\n", + " \"constraint_check_type\": \"LessThanThreshold\",\n", + " \"description\": \"Metric weightedF1 with 0.34519661584972283 +/- 0.004997774377359799 was LessThanThreshold '0.9247293447293448'\",\n", + " \"metric_name\": \"weightedF1\"\n", + " }\n", + " ]\n", + "}\n" + ] + } + ], + "source": [ + "describe_model_monitoring_schedule(ENDPOINT)" + ] + }, + { + "cell_type": "markdown", + "id": "38c3d9f6", + "metadata": {}, + "source": [ + "### Step 14 - Tearing Down Resources\n", + "\n", + "The following code will stop the monitoring jobs and delete the endpoint." + ] + }, + { + "cell_type": "code", + "execution_count": 783, + "id": "bb74dc04-54a1-4a3f-854f-4877f7f0b4a1", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Monitoring schedule deleted.\n", + "There's no ModelQuality Monitoring Schedule.\n" + ] + } + ], + "source": [ + "#| code: true\n", + "#| output: false\n", + "\n", + "delete_data_monitoring_schedule(ENDPOINT)\n", + "delete_model_monitoring_schedule(ENDPOINT)" + ] + }, + { + "cell_type": "markdown", + "id": "c97e5419", + "metadata": {}, + "source": [ + "Let's delete the endpoint:" + ] + }, + { + "cell_type": "markdown", + "id": "307f5062", + "metadata": {}, + "source": [ + "#| hide\n", + "\n", + "
Note: \n", + " The %%script cell magic is a convenient way to prevent the notebook from executing a specific cell. If you want to run the cell, comment out the line containing the %%script cell magic.\n", + "
" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9eabe84e", + "metadata": {}, + "outputs": [], + "source": [ + "%%script false --no-raise-error\n", + "#| eval: false\n", + "#| code: true\n", + "#| output: false\n", + "\n", + "predictor.delete_endpoint()" + ] + }, + { + "cell_type": "markdown", + "id": "db0d6d8d-791c-4ae0-ba79-e0da33d0ece2", + "metadata": {}, + "source": [ + "### Assignments\n", + "\n", + "* Assignment 5.1 You can visualize the results of your monitoring jobs in Amazon SageMaker Studio. Go to your endpoint, and visit the Data quality and Model quality tabs. View the details of your monitoring jobs, and create a few charts to explore the baseline and the captured values for any metric that the monitoring job calculates.\n", + "\n", + "* Assignment 5.2 The QualityCheck Step runs a processing job to compute baseline statistics and constraints from the input dataset. We configured the pipeline to generate the initial baselines every time it runs. Modify the code to prevent the pipeline from registering a new version of the model if the dataset violates the baseline of the previous model version. You can configure the QualityCheck Step to accomplish this.\n", + "\n", + "* Assignment 5.3 We are generating predictions for the test set twice during the execution of our pipeline. First, during the Evaluation Step, and then using a Transform Step in anticipation of generating the baseline to monitor the model. Modify the Evaluation Step so it reuses the model performance computed by the QualityCheck Step instead of generating predictions again.\n", + "\n", + "* Assignment 5.4 [Evidently AI](https://evidentlyai.com/) is an open-source Machine Learning observability platform that you can use to evaluate, test, and monitor models. For this assignment, integrate the endpoint we built with Evidently AI to use its capabilities to monitor the model.\n", + "\n", + "* Assignment 5.5 Instead of running the entire pipeline from start to finish, sometimes you may only need to iterate over particular steps. SageMaker Pipelines supports [Selective Execution for Pipeline Steps](https://docs.aws.amazon.com/sagemaker/latest/dg/pipelines-selective-ex.html). In this assignment you will use Selective Execution to only run one specific step of the pipeline. [Unlocking efficiency: Harnessing the power of Selective Execution in Amazon SageMaker Pipelines](https://aws.amazon.com/blogs/machine-learning/unlocking-efficiency-harnessing-the-power-of-selective-execution-in-amazon-sagemaker-pipelines/) is a great article that explains this feature." + ] + } + ], + "metadata": { + "availableInstances": [ + { + "_defaultOrder": 0, + "_isFastLaunch": true, + "category": "General purpose", + "gpuNum": 0, + "hideHardwareSpecs": false, + "memoryGiB": 4, + "name": "ml.t3.medium", + "vcpuNum": 2 + }, + { + "_defaultOrder": 1, + "_isFastLaunch": false, + "category": "General purpose", + "gpuNum": 0, + "hideHardwareSpecs": false, + "memoryGiB": 8, + "name": "ml.t3.large", + "vcpuNum": 2 + }, + { + "_defaultOrder": 2, + "_isFastLaunch": false, + "category": "General purpose", + "gpuNum": 0, + "hideHardwareSpecs": false, + "memoryGiB": 16, + "name": "ml.t3.xlarge", + "vcpuNum": 4 + }, + { + "_defaultOrder": 3, + "_isFastLaunch": false, + "category": "General purpose", + "gpuNum": 0, + "hideHardwareSpecs": false, + "memoryGiB": 32, + "name": "ml.t3.2xlarge", + "vcpuNum": 8 + }, + { + "_defaultOrder": 4, + "_isFastLaunch": true, + "category": "General purpose", + "gpuNum": 0, + "hideHardwareSpecs": false, + "memoryGiB": 8, + "name": "ml.m5.large", + "vcpuNum": 2 + }, + { + "_defaultOrder": 5, + "_isFastLaunch": false, + "category": "General purpose", + "gpuNum": 0, + "hideHardwareSpecs": false, + "memoryGiB": 16, + "name": "ml.m5.xlarge", + "vcpuNum": 4 + }, + { + "_defaultOrder": 6, + "_isFastLaunch": false, + "category": "General purpose", + "gpuNum": 0, + "hideHardwareSpecs": false, + "memoryGiB": 32, + "name": "ml.m5.2xlarge", + "vcpuNum": 8 + }, + { + "_defaultOrder": 7, + "_isFastLaunch": false, + "category": "General purpose", + "gpuNum": 0, + "hideHardwareSpecs": false, + "memoryGiB": 64, + "name": "ml.m5.4xlarge", + "vcpuNum": 16 + }, + { + "_defaultOrder": 8, + "_isFastLaunch": false, + "category": "General purpose", + "gpuNum": 0, + "hideHardwareSpecs": false, + "memoryGiB": 128, + "name": "ml.m5.8xlarge", + "vcpuNum": 32 + }, + { + "_defaultOrder": 9, + "_isFastLaunch": false, + "category": "General purpose", + "gpuNum": 0, + "hideHardwareSpecs": false, + "memoryGiB": 192, + "name": "ml.m5.12xlarge", + "vcpuNum": 48 + }, + { + "_defaultOrder": 10, + "_isFastLaunch": false, + "category": "General purpose", + "gpuNum": 0, + "hideHardwareSpecs": false, + "memoryGiB": 256, + "name": "ml.m5.16xlarge", + "vcpuNum": 64 + }, + { + "_defaultOrder": 11, + "_isFastLaunch": false, + "category": "General purpose", + "gpuNum": 0, + "hideHardwareSpecs": false, + "memoryGiB": 384, + "name": "ml.m5.24xlarge", + "vcpuNum": 96 + }, + { + "_defaultOrder": 12, + "_isFastLaunch": false, + "category": "General purpose", + "gpuNum": 0, + "hideHardwareSpecs": false, + "memoryGiB": 8, + "name": "ml.m5d.large", + "vcpuNum": 2 + }, + { + "_defaultOrder": 13, + "_isFastLaunch": false, + "category": "General purpose", + "gpuNum": 0, + "hideHardwareSpecs": false, + "memoryGiB": 16, + "name": "ml.m5d.xlarge", + "vcpuNum": 4 + }, + { + "_defaultOrder": 14, + "_isFastLaunch": false, + "category": "General purpose", + "gpuNum": 0, + "hideHardwareSpecs": false, + "memoryGiB": 32, + "name": "ml.m5d.2xlarge", + "vcpuNum": 8 + }, + { + "_defaultOrder": 15, + "_isFastLaunch": false, + "category": "General purpose", + "gpuNum": 0, + "hideHardwareSpecs": false, + "memoryGiB": 64, + "name": "ml.m5d.4xlarge", + "vcpuNum": 16 + }, + { + "_defaultOrder": 16, + "_isFastLaunch": false, + "category": "General purpose", + "gpuNum": 0, + "hideHardwareSpecs": false, + "memoryGiB": 128, + "name": "ml.m5d.8xlarge", + "vcpuNum": 32 + }, + { + "_defaultOrder": 17, + "_isFastLaunch": false, + "category": "General purpose", + "gpuNum": 0, + "hideHardwareSpecs": false, + "memoryGiB": 192, + "name": "ml.m5d.12xlarge", + "vcpuNum": 48 + }, + { + "_defaultOrder": 18, + "_isFastLaunch": false, + "category": "General purpose", + "gpuNum": 0, + "hideHardwareSpecs": false, + "memoryGiB": 256, + "name": "ml.m5d.16xlarge", + "vcpuNum": 64 + }, + { + "_defaultOrder": 19, + "_isFastLaunch": false, + "category": "General purpose", + "gpuNum": 0, + "hideHardwareSpecs": false, + "memoryGiB": 384, + "name": "ml.m5d.24xlarge", + "vcpuNum": 96 + }, + { + "_defaultOrder": 20, + "_isFastLaunch": false, + "category": "General purpose", + "gpuNum": 0, + "hideHardwareSpecs": true, + "memoryGiB": 0, + "name": "ml.geospatial.interactive", + "supportedImageNames": [ + "sagemaker-geospatial-v1-0" + ], + "vcpuNum": 0 + }, + { + "_defaultOrder": 21, + "_isFastLaunch": true, + "category": "Compute optimized", + "gpuNum": 0, + "hideHardwareSpecs": false, + "memoryGiB": 4, + "name": "ml.c5.large", + "vcpuNum": 2 + }, + { + "_defaultOrder": 22, + "_isFastLaunch": false, + "category": "Compute optimized", + "gpuNum": 0, + "hideHardwareSpecs": false, + "memoryGiB": 8, + "name": "ml.c5.xlarge", + "vcpuNum": 4 + }, + { + "_defaultOrder": 23, + "_isFastLaunch": false, + "category": "Compute optimized", + "gpuNum": 0, + "hideHardwareSpecs": false, + "memoryGiB": 16, + "name": "ml.c5.2xlarge", + "vcpuNum": 8 + }, + { + "_defaultOrder": 24, + "_isFastLaunch": false, + "category": "Compute optimized", + "gpuNum": 0, + "hideHardwareSpecs": false, + "memoryGiB": 32, + "name": "ml.c5.4xlarge", + "vcpuNum": 16 + }, + { + "_defaultOrder": 25, + "_isFastLaunch": false, + "category": "Compute optimized", + "gpuNum": 0, + "hideHardwareSpecs": false, + "memoryGiB": 72, + "name": "ml.c5.9xlarge", + "vcpuNum": 36 + }, + { + "_defaultOrder": 26, + "_isFastLaunch": false, + "category": "Compute optimized", + "gpuNum": 0, + "hideHardwareSpecs": false, + "memoryGiB": 96, + "name": "ml.c5.12xlarge", + "vcpuNum": 48 + }, + { + "_defaultOrder": 27, + "_isFastLaunch": false, + "category": "Compute optimized", + "gpuNum": 0, + "hideHardwareSpecs": false, + "memoryGiB": 144, + "name": "ml.c5.18xlarge", + "vcpuNum": 72 + }, + { + "_defaultOrder": 28, + "_isFastLaunch": false, + "category": "Compute optimized", + "gpuNum": 0, + "hideHardwareSpecs": false, + "memoryGiB": 192, + "name": "ml.c5.24xlarge", + "vcpuNum": 96 + }, + { + "_defaultOrder": 29, + "_isFastLaunch": true, + "category": "Accelerated computing", + "gpuNum": 1, + "hideHardwareSpecs": false, + "memoryGiB": 16, + "name": "ml.g4dn.xlarge", + "vcpuNum": 4 + }, + { + "_defaultOrder": 30, + "_isFastLaunch": false, + "category": "Accelerated computing", + "gpuNum": 1, + "hideHardwareSpecs": false, + "memoryGiB": 32, + "name": "ml.g4dn.2xlarge", + "vcpuNum": 8 + }, + { + "_defaultOrder": 31, + "_isFastLaunch": false, + "category": "Accelerated computing", + "gpuNum": 1, + "hideHardwareSpecs": false, + "memoryGiB": 64, + "name": "ml.g4dn.4xlarge", + "vcpuNum": 16 + }, + { + "_defaultOrder": 32, + "_isFastLaunch": false, + "category": "Accelerated computing", + "gpuNum": 1, + "hideHardwareSpecs": false, + "memoryGiB": 128, + "name": "ml.g4dn.8xlarge", + "vcpuNum": 32 + }, + { + "_defaultOrder": 33, + "_isFastLaunch": false, + "category": "Accelerated computing", + "gpuNum": 4, + "hideHardwareSpecs": false, + "memoryGiB": 192, + "name": "ml.g4dn.12xlarge", + "vcpuNum": 48 + }, + { + "_defaultOrder": 34, + "_isFastLaunch": false, + "category": "Accelerated computing", + "gpuNum": 1, + "hideHardwareSpecs": false, + "memoryGiB": 256, + "name": "ml.g4dn.16xlarge", + "vcpuNum": 64 + }, + { + "_defaultOrder": 35, + "_isFastLaunch": false, + "category": "Accelerated computing", + "gpuNum": 1, + "hideHardwareSpecs": false, + "memoryGiB": 61, + "name": "ml.p3.2xlarge", + "vcpuNum": 8 + }, + { + "_defaultOrder": 36, + "_isFastLaunch": false, + "category": "Accelerated computing", + "gpuNum": 4, + "hideHardwareSpecs": false, + "memoryGiB": 244, + "name": "ml.p3.8xlarge", + "vcpuNum": 32 + }, + { + "_defaultOrder": 37, + "_isFastLaunch": false, + "category": "Accelerated computing", + "gpuNum": 8, + "hideHardwareSpecs": false, + "memoryGiB": 488, + "name": "ml.p3.16xlarge", + "vcpuNum": 64 + }, + { + "_defaultOrder": 38, + "_isFastLaunch": false, + "category": "Accelerated computing", + "gpuNum": 8, + "hideHardwareSpecs": false, + "memoryGiB": 768, + "name": "ml.p3dn.24xlarge", + "vcpuNum": 96 + }, + { + "_defaultOrder": 39, + "_isFastLaunch": false, + "category": "Memory Optimized", + "gpuNum": 0, + "hideHardwareSpecs": false, + "memoryGiB": 16, + "name": "ml.r5.large", + "vcpuNum": 2 + }, + { + "_defaultOrder": 40, + "_isFastLaunch": false, + "category": "Memory Optimized", + "gpuNum": 0, + "hideHardwareSpecs": false, + "memoryGiB": 32, + "name": "ml.r5.xlarge", + "vcpuNum": 4 + }, + { + "_defaultOrder": 41, + "_isFastLaunch": false, + "category": "Memory Optimized", + "gpuNum": 0, + "hideHardwareSpecs": false, + "memoryGiB": 64, + "name": "ml.r5.2xlarge", + "vcpuNum": 8 + }, + { + "_defaultOrder": 42, + "_isFastLaunch": false, + "category": "Memory Optimized", + "gpuNum": 0, + "hideHardwareSpecs": false, + "memoryGiB": 128, + "name": "ml.r5.4xlarge", + "vcpuNum": 16 + }, + { + "_defaultOrder": 43, + "_isFastLaunch": false, + "category": "Memory Optimized", + "gpuNum": 0, + "hideHardwareSpecs": false, + "memoryGiB": 256, + "name": "ml.r5.8xlarge", + "vcpuNum": 32 + }, + { + "_defaultOrder": 44, + "_isFastLaunch": false, + "category": "Memory Optimized", + "gpuNum": 0, + "hideHardwareSpecs": false, + "memoryGiB": 384, + "name": "ml.r5.12xlarge", + "vcpuNum": 48 + }, + { + "_defaultOrder": 45, + "_isFastLaunch": false, + "category": "Memory Optimized", + "gpuNum": 0, + "hideHardwareSpecs": false, + "memoryGiB": 512, + "name": "ml.r5.16xlarge", + "vcpuNum": 64 + }, + { + "_defaultOrder": 46, + "_isFastLaunch": false, + "category": "Memory Optimized", + "gpuNum": 0, + "hideHardwareSpecs": false, + "memoryGiB": 768, + "name": "ml.r5.24xlarge", + "vcpuNum": 96 + }, + { + "_defaultOrder": 47, + "_isFastLaunch": false, + "category": "Accelerated computing", + "gpuNum": 1, + "hideHardwareSpecs": false, + "memoryGiB": 16, + "name": "ml.g5.xlarge", + "vcpuNum": 4 + }, + { + "_defaultOrder": 48, + "_isFastLaunch": false, + "category": "Accelerated computing", + "gpuNum": 1, + "hideHardwareSpecs": false, + "memoryGiB": 32, + "name": "ml.g5.2xlarge", + "vcpuNum": 8 + }, + { + "_defaultOrder": 49, + "_isFastLaunch": false, + "category": "Accelerated computing", + "gpuNum": 1, + "hideHardwareSpecs": false, + "memoryGiB": 64, + "name": "ml.g5.4xlarge", + "vcpuNum": 16 + }, + { + "_defaultOrder": 50, + "_isFastLaunch": false, + "category": "Accelerated computing", + "gpuNum": 1, + "hideHardwareSpecs": false, + "memoryGiB": 128, + "name": "ml.g5.8xlarge", + "vcpuNum": 32 + }, + { + "_defaultOrder": 51, + "_isFastLaunch": false, + "category": "Accelerated computing", + "gpuNum": 1, + "hideHardwareSpecs": false, + "memoryGiB": 256, + "name": "ml.g5.16xlarge", + "vcpuNum": 64 + }, + { + "_defaultOrder": 52, + "_isFastLaunch": false, + "category": "Accelerated computing", + "gpuNum": 4, + "hideHardwareSpecs": false, + "memoryGiB": 192, + "name": "ml.g5.12xlarge", + "vcpuNum": 48 + }, + { + "_defaultOrder": 53, + "_isFastLaunch": false, + "category": "Accelerated computing", + "gpuNum": 4, + "hideHardwareSpecs": false, + "memoryGiB": 384, + "name": "ml.g5.24xlarge", + "vcpuNum": 96 + }, + { + "_defaultOrder": 54, + "_isFastLaunch": false, + "category": "Accelerated computing", + "gpuNum": 8, + "hideHardwareSpecs": false, + "memoryGiB": 768, + "name": "ml.g5.48xlarge", + "vcpuNum": 192 + }, + { + "_defaultOrder": 55, + "_isFastLaunch": false, + "category": "Accelerated computing", + "gpuNum": 8, + "hideHardwareSpecs": false, + "memoryGiB": 1152, + "name": "ml.p4d.24xlarge", + "vcpuNum": 96 + }, + { + "_defaultOrder": 56, + "_isFastLaunch": false, + "category": "Accelerated computing", + "gpuNum": 8, + "hideHardwareSpecs": false, + "memoryGiB": 1152, + "name": "ml.p4de.24xlarge", + "vcpuNum": 96 + } + ], + "instance_type": "ml.t3.medium", + "kernelspec": { + "display_name": ".venv", + "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.9.13" + }, + "lcc_arn": "arn:aws:sagemaker:us-east-1:325223348818:studio-lifecycle-config/packages", + "toc-autonumbering": false, + "toc-showcode": false, + "toc-showmarkdowntxt": false + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/program/cohort.ipynb b/program/cohort.ipynb index 8376552..14860f6 100644 --- a/program/cohort.ipynb +++ b/program/cohort.ipynb @@ -42,7 +42,7 @@ }, { "cell_type": "code", - "execution_count": 589, + "execution_count": 640, "id": "4b2265b0", "metadata": {}, "outputs": [ @@ -101,7 +101,7 @@ }, { "cell_type": "code", - "execution_count": 590, + "execution_count": 641, "id": "32c4d764", "metadata": {}, "outputs": [], @@ -119,7 +119,7 @@ }, { "cell_type": "code", - "execution_count": 591, + "execution_count": 642, "id": "3164a3af", "metadata": {}, "outputs": [], @@ -142,7 +142,7 @@ }, { "cell_type": "code", - "execution_count": 592, + "execution_count": 643, "id": "7bc40d28", "metadata": {}, "outputs": [], @@ -161,7 +161,7 @@ }, { "cell_type": "code", - "execution_count": 593, + "execution_count": 644, "id": "3b3f17e5", "metadata": {}, "outputs": [], @@ -201,7 +201,7 @@ }, { "cell_type": "code", - "execution_count": 594, + "execution_count": 645, "id": "942a01b5", "metadata": {}, "outputs": [], @@ -242,7 +242,7 @@ }, { "cell_type": "code", - "execution_count": 595, + "execution_count": 646, "id": "f1cd2f0e-446d-48a9-a008-b4f1cc593bfc", "metadata": { "tags": [] @@ -349,7 +349,7 @@ "4 3450.0 FEMALE " ] }, - "execution_count": 595, + "execution_count": 646, "metadata": {}, "output_type": "execute_result" } @@ -386,7 +386,7 @@ }, { "cell_type": "code", - "execution_count": 596, + "execution_count": 647, "id": "f2107c25-e730-4e22-a1b8-5bda53e61124", "metadata": { "tags": [] @@ -565,7 +565,7 @@ "max 6300.000000 NaN " ] }, - "execution_count": 596, + "execution_count": 647, "metadata": {}, "output_type": "execute_result" } @@ -584,7 +584,7 @@ }, { "cell_type": "code", - "execution_count": 597, + "execution_count": 648, "id": "1242122a-726e-4c37-a718-dd8e873d1612", "metadata": { "tags": [] @@ -642,7 +642,7 @@ }, { "cell_type": "code", - "execution_count": 598, + "execution_count": 649, "id": "cf1cf582-8831-4f83-bb17-2175afb193e8", "metadata": { "tags": [] @@ -657,7 +657,7 @@ "Name: count, dtype: int64" ] }, - "execution_count": 598, + "execution_count": 649, "metadata": {}, "output_type": "execute_result" } @@ -677,7 +677,7 @@ }, { "cell_type": "code", - "execution_count": 599, + "execution_count": 650, "id": "cc42cb08-275c-4b05-9d2b-77052da2f336", "metadata": { "tags": [] @@ -696,7 +696,7 @@ "dtype: int64" ] }, - "execution_count": 599, + "execution_count": 650, "metadata": {}, "output_type": "execute_result" } @@ -715,7 +715,7 @@ }, { "cell_type": "code", - "execution_count": 600, + "execution_count": 651, "id": "3c57d55d-afd6-467a-a7a8-ff04132770ed", "metadata": { "tags": [] @@ -734,7 +734,7 @@ "dtype: int64" ] }, - "execution_count": 600, + "execution_count": 651, "metadata": {}, "output_type": "execute_result" } @@ -757,7 +757,7 @@ }, { "cell_type": "code", - "execution_count": 601, + "execution_count": 652, "id": "2852c740", "metadata": {}, "outputs": [ @@ -803,7 +803,7 @@ }, { "cell_type": "code", - "execution_count": 602, + "execution_count": 653, "id": "707cc972", "metadata": {}, "outputs": [ @@ -851,7 +851,7 @@ }, { "cell_type": "code", - "execution_count": 603, + "execution_count": 654, "id": "3daf3ba1-d218-4ad4-b862-af679b91273f", "metadata": { "tags": [] @@ -931,7 +931,7 @@ "body_mass_g 640316.716388 " ] }, - "execution_count": 603, + "execution_count": 654, "metadata": {}, "output_type": "execute_result" } @@ -956,7 +956,7 @@ }, { "cell_type": "code", - "execution_count": 604, + "execution_count": 655, "id": "1d793e09-2cb9-47ff-a0e6-199a0f4fc1b3", "metadata": { "tags": [] @@ -1036,7 +1036,7 @@ "body_mass_g 1.000000 " ] }, - "execution_count": 604, + "execution_count": 655, "metadata": {}, "output_type": "execute_result" } @@ -1061,7 +1061,7 @@ }, { "cell_type": "code", - "execution_count": 605, + "execution_count": 656, "id": "1258c99d", "metadata": {}, "outputs": [ @@ -1101,7 +1101,7 @@ }, { "cell_type": "code", - "execution_count": 606, + "execution_count": 657, "id": "45b0a87f-028d-477f-9b65-199728c0b7ee", "metadata": { "tags": [] @@ -1155,7 +1155,7 @@ }, { "cell_type": "code", - "execution_count": 607, + "execution_count": 658, "id": "fb6ba7c0-1bd6-4fe5-8b7f-f6cbdfd3846c", "metadata": { "tags": [] @@ -1351,7 +1351,7 @@ }, { "cell_type": "code", - "execution_count": 608, + "execution_count": 659, "id": "d1f122a4-acff-4687-91b9-bfef13567d88", "metadata": { "tags": [] @@ -1362,7 +1362,7 @@ "output_type": "stream", "text": [ "\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\n", - "\u001b[32m\u001b[32m\u001b[1m8 passed\u001b[0m\u001b[32m in 0.17s\u001b[0m\u001b[0m\n" + "\u001b[32m\u001b[32m\u001b[1m8 passed\u001b[0m\u001b[32m in 0.16s\u001b[0m\u001b[0m\n" ] } ], @@ -1489,7 +1489,7 @@ }, { "cell_type": "code", - "execution_count": 609, + "execution_count": 660, "id": "d88e9ccf", "metadata": {}, "outputs": [], @@ -1509,7 +1509,7 @@ }, { "cell_type": "code", - "execution_count": 610, + "execution_count": 661, "id": "331fe373", "metadata": {}, "outputs": [], @@ -1532,7 +1532,7 @@ }, { "cell_type": "code", - "execution_count": 611, + "execution_count": 662, "id": "3aa4471a", "metadata": {}, "outputs": [ @@ -1573,7 +1573,7 @@ }, { "cell_type": "code", - "execution_count": 612, + "execution_count": 663, "id": "cdbd9303", "metadata": { "tags": [] @@ -1654,7 +1654,7 @@ }, { "cell_type": "code", - "execution_count": 613, + "execution_count": 664, "id": "e140642a", "metadata": { "tags": [] @@ -1664,16 +1664,16 @@ "data": { "text/plain": [ "{'PipelineArn': 'arn:aws:sagemaker:us-east-1:325223348818:pipeline/session1-pipeline',\n", - " 'ResponseMetadata': {'RequestId': '10ebb4eb-c57e-4b9d-b94a-dc6307f0207e',\n", + " 'ResponseMetadata': {'RequestId': '02b62dd1-6de0-4723-9019-f4f72862ba5c',\n", " 'HTTPStatusCode': 200,\n", - " 'HTTPHeaders': {'x-amzn-requestid': '10ebb4eb-c57e-4b9d-b94a-dc6307f0207e',\n", + " 'HTTPHeaders': {'x-amzn-requestid': '02b62dd1-6de0-4723-9019-f4f72862ba5c',\n", " 'content-type': 'application/x-amz-json-1.1',\n", " 'content-length': '85',\n", - " 'date': 'Thu, 26 Oct 2023 18:42:58 GMT'},\n", + " 'date': 'Fri, 27 Oct 2023 14:38:36 GMT'},\n", " 'RetryAttempts': 0}}" ] }, - "execution_count": 613, + "execution_count": 664, "metadata": {}, "output_type": "execute_result" } @@ -1722,7 +1722,7 @@ }, { "cell_type": "code", - "execution_count": 614, + "execution_count": 665, "id": "59d1e634", "metadata": {}, "outputs": [], @@ -1780,7 +1780,7 @@ }, { "cell_type": "code", - "execution_count": 615, + "execution_count": 666, "id": "d92b121d-dcb9-43e8-9ee3-3ececb583e7e", "metadata": { "tags": [] @@ -1889,7 +1889,7 @@ }, { "cell_type": "code", - "execution_count": 616, + "execution_count": 667, "id": "14ea27ce-c453-4cb0-b309-dbecd732957e", "metadata": { "tags": [] @@ -1906,16 +1906,16 @@ "name": "stdout", "output_type": "stream", "text": [ - "8/8 - 0s - loss: 1.0050 - accuracy: 0.4561 - val_loss: 0.9934 - val_accuracy: 0.4118 - 239ms/epoch - 30ms/step\n", + "8/8 - 0s - loss: 1.0173 - accuracy: 0.4728 - val_loss: 0.9260 - val_accuracy: 0.6078 - 230ms/epoch - 29ms/step\n", "2/2 [==============================] - 0s 1ms/step\n", - "Validation accuracy: 0.4117647058823529\n" + "Validation accuracy: 0.6078431372549019\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "INFO:tensorflow:Assets written to: /var/folders/4c/v1q3hy1x4mb5w0wpc72zl3_w0000gp/T/tmp_d8wmtx2/model/001/assets\n" + "INFO:tensorflow:Assets written to: /var/folders/4c/v1q3hy1x4mb5w0wpc72zl3_w0000gp/T/tmpv4apdp15/model/001/assets\n" ] }, { @@ -1923,7 +1923,7 @@ "output_type": "stream", "text": [ "\u001b[32m.\u001b[0m\n", - "\u001b[32m\u001b[32m\u001b[1m1 passed\u001b[0m\u001b[32m in 0.55s\u001b[0m\u001b[0m\n" + "\u001b[32m\u001b[32m\u001b[1m1 passed\u001b[0m\u001b[32m in 0.53s\u001b[0m\u001b[0m\n" ] } ], @@ -1992,7 +1992,7 @@ }, { "cell_type": "code", - "execution_count": 617, + "execution_count": 668, "id": "90fe82ae-6a2c-4461-bc83-bb52d8871e3b", "metadata": { "tags": [] @@ -2047,21 +2047,12 @@ }, { "cell_type": "code", - "execution_count": 618, + "execution_count": 738, "id": "99e4850c-83d6-4f4e-a813-d5a3f4bb7486", "metadata": { "tags": [] }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/Users/svpino/dev/ml.school/.venv/lib/python3.9/site-packages/sagemaker/workflow/pipeline_context.py:297: UserWarning: Running within a PipelineSession, there will be No Wait, No Logs, and No Job being started.\n", - " warnings.warn(\n" - ] - } - ], + "outputs": [], "source": [ "# | code: true\n", "# | output: false\n", @@ -2069,7 +2060,6 @@ "from sagemaker.workflow.steps import TrainingStep\n", "from sagemaker.inputs import TrainingInput\n", "\n", - "\n", "train_model_step = TrainingStep(\n", " name=\"train-model\",\n", " step_args=estimator.fit(\n", @@ -2112,7 +2102,7 @@ }, { "cell_type": "code", - "execution_count": 619, + "execution_count": 670, "id": "f367d0e3", "metadata": {}, "outputs": [], @@ -2143,7 +2133,7 @@ }, { "cell_type": "code", - "execution_count": 620, + "execution_count": 671, "id": "c8c82750", "metadata": {}, "outputs": [], @@ -2174,7 +2164,7 @@ }, { "cell_type": "code", - "execution_count": 621, + "execution_count": 672, "id": "038ff2e5-ed28-445b-bc03-4e996ec2286f", "metadata": { "tags": [] @@ -2217,7 +2207,7 @@ }, { "cell_type": "code", - "execution_count": 622, + "execution_count": 673, "id": "9799ab39-fcae-41f4-a68b-85ab71b3ba9a", "metadata": { "tags": [] @@ -2227,9 +2217,6 @@ "name": "stderr", "output_type": "stream", "text": [ - "INFO:sagemaker.image_uris:image_uri is not presented, retrieving image_uri based on instance_type, framework etc.\n", - "WARNING:sagemaker.estimator:No finished training job found associated with this estimator. Please make sure this estimator is only used for building workflow config\n", - "WARNING:sagemaker.estimator:No finished training job found associated with this estimator. Please make sure this estimator is only used for building workflow config\n", "INFO:sagemaker.image_uris:image_uri is not presented, retrieving image_uri based on instance_type, framework etc.\n" ] }, @@ -2244,9 +2231,6 @@ "name": "stderr", "output_type": "stream", "text": [ - "INFO:sagemaker.image_uris:image_uri is not presented, retrieving image_uri based on instance_type, framework etc.\n", - "WARNING:sagemaker.estimator:No finished training job found associated with this estimator. Please make sure this estimator is only used for building workflow config\n", - "WARNING:sagemaker.estimator:No finished training job found associated with this estimator. Please make sure this estimator is only used for building workflow config\n", "INFO:sagemaker.image_uris:image_uri is not presented, retrieving image_uri based on instance_type, framework etc.\n" ] }, @@ -2261,16 +2245,16 @@ "data": { "text/plain": [ "{'PipelineArn': 'arn:aws:sagemaker:us-east-1:325223348818:pipeline/session2-pipeline',\n", - " 'ResponseMetadata': {'RequestId': '35562718-eb3c-4377-b5c3-bd7bd65f2077',\n", + " 'ResponseMetadata': {'RequestId': 'e99208aa-4074-41aa-a12b-90af6da62e3f',\n", " 'HTTPStatusCode': 200,\n", - " 'HTTPHeaders': {'x-amzn-requestid': '35562718-eb3c-4377-b5c3-bd7bd65f2077',\n", + " 'HTTPHeaders': {'x-amzn-requestid': 'e99208aa-4074-41aa-a12b-90af6da62e3f',\n", " 'content-type': 'application/x-amz-json-1.1',\n", " 'content-length': '85',\n", - " 'date': 'Thu, 26 Oct 2023 18:43:00 GMT'},\n", + " 'date': 'Fri, 27 Oct 2023 14:38:38 GMT'},\n", " 'RetryAttempts': 0}}" ] }, - "execution_count": 622, + "execution_count": 673, "metadata": {}, "output_type": "execute_result" } @@ -2315,7 +2299,7 @@ }, { "cell_type": "code", - "execution_count": 623, + "execution_count": 674, "id": "274a9b1e", "metadata": {}, "outputs": [], @@ -2375,7 +2359,7 @@ }, { "cell_type": "code", - "execution_count": 624, + "execution_count": 675, "id": "3ee3ab26-afa5-4ceb-9f7a-005d5fdea646", "metadata": { "tags": [] @@ -2461,7 +2445,7 @@ }, { "cell_type": "code", - "execution_count": 625, + "execution_count": 676, "id": "9a2540d8-278a-4953-bc54-0469d154427d", "metadata": { "tags": [] @@ -2478,16 +2462,16 @@ "name": "stdout", "output_type": "stream", "text": [ - "8/8 - 0s - loss: 1.2371 - accuracy: 0.3138 - val_loss: 1.0864 - val_accuracy: 0.4902 - 237ms/epoch - 30ms/step\n", + "8/8 - 0s - loss: 1.1330 - accuracy: 0.4142 - val_loss: 1.1001 - val_accuracy: 0.5098 - 236ms/epoch - 30ms/step\n", "2/2 [==============================] - 0s 1ms/step\n", - "Validation accuracy: 0.49019607843137253\n" + "Validation accuracy: 0.5098039215686274\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "INFO:tensorflow:Assets written to: /var/folders/4c/v1q3hy1x4mb5w0wpc72zl3_w0000gp/T/tmp6a4kt1az/model/001/assets\n", + "INFO:tensorflow:Assets written to: /var/folders/4c/v1q3hy1x4mb5w0wpc72zl3_w0000gp/T/tmpprbc5h18/model/001/assets\n", "WARNING:absl:At this time, the v2.11+ optimizer `tf.keras.optimizers.RestoredOptimizer` runs slowly on M1/M2 Macs, please use the legacy Keras optimizer instead, located at `tf.keras.optimizers.legacy.RestoredOptimizer`.\n", "WARNING:absl:At this time, the v2.11+ optimizer `tf.keras.optimizers.SGD` runs slowly on M1/M2 Macs, please use the legacy Keras optimizer instead, located at `tf.keras.optimizers.legacy.SGD`.\n" ] @@ -2496,8 +2480,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "2/2 [==============================] - 0s 2ms/step\n", - "Test accuracy: 0.35294117647058826\n", + "2/2 [==============================] - 0s 1ms/step\n", + "Test accuracy: 0.4117647058823529\n", "\u001b[32m.\u001b[0m" ] }, @@ -2512,22 +2496,16 @@ "name": "stdout", "output_type": "stream", "text": [ - "8/8 - 0s - loss: 1.3224 - accuracy: 0.2385 - val_loss: 1.2449 - val_accuracy: 0.1765 - 232ms/epoch - 29ms/step\n", - "2/2 [==============================] - 0s 1ms/step\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Validation accuracy: 0.17647058823529413\n" + "8/8 - 0s - loss: 1.0329 - accuracy: 0.4644 - val_loss: 0.9795 - val_accuracy: 0.5882 - 235ms/epoch - 29ms/step\n", + "2/2 [==============================] - 0s 1ms/step\n", + "Validation accuracy: 0.5882352941176471\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "INFO:tensorflow:Assets written to: /var/folders/4c/v1q3hy1x4mb5w0wpc72zl3_w0000gp/T/tmpq00sk8jn/model/001/assets\n", + "INFO:tensorflow:Assets written to: /var/folders/4c/v1q3hy1x4mb5w0wpc72zl3_w0000gp/T/tmph0nj0wfb/model/001/assets\n", "WARNING:absl:At this time, the v2.11+ optimizer `tf.keras.optimizers.RestoredOptimizer` runs slowly on M1/M2 Macs, please use the legacy Keras optimizer instead, located at `tf.keras.optimizers.legacy.RestoredOptimizer`.\n", "WARNING:absl:At this time, the v2.11+ optimizer `tf.keras.optimizers.SGD` runs slowly on M1/M2 Macs, please use the legacy Keras optimizer instead, located at `tf.keras.optimizers.legacy.SGD`.\n" ] @@ -2536,8 +2514,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "2/2 [==============================] - 0s 1ms/step\n", - "Test accuracy: 0.21568627450980393\n", + "2/2 [==============================] - 0s 2ms/step\n", + "Test accuracy: 0.5686274509803921\n", "\u001b[32m.\u001b[0m\n", "\u001b[32m\u001b[32m\u001b[1m2 passed\u001b[0m\u001b[32m in 1.35s\u001b[0m\u001b[0m\n" ] @@ -2622,7 +2600,7 @@ }, { "cell_type": "code", - "execution_count": 626, + "execution_count": 677, "id": "2fdff07f", "metadata": {}, "outputs": [ @@ -2662,7 +2640,7 @@ }, { "cell_type": "code", - "execution_count": 627, + "execution_count": 678, "id": "4f19e15b", "metadata": {}, "outputs": [], @@ -2685,7 +2663,7 @@ }, { "cell_type": "code", - "execution_count": 628, + "execution_count": 679, "id": "1f27b2ef", "metadata": {}, "outputs": [], @@ -2707,7 +2685,7 @@ }, { "cell_type": "code", - "execution_count": 629, + "execution_count": 680, "id": "48139a07-5c8e-4bc6-b666-bf9531f7f520", "metadata": { "tags": [] @@ -2774,7 +2752,7 @@ }, { "cell_type": "code", - "execution_count": 630, + "execution_count": 681, "id": "bb70f907", "metadata": {}, "outputs": [], @@ -2792,7 +2770,7 @@ }, { "cell_type": "code", - "execution_count": 631, + "execution_count": 682, "id": "4ca4cb61", "metadata": {}, "outputs": [], @@ -2818,7 +2796,7 @@ }, { "cell_type": "code", - "execution_count": 632, + "execution_count": 683, "id": "8c05a7e1", "metadata": {}, "outputs": [], @@ -2852,7 +2830,7 @@ }, { "cell_type": "code", - "execution_count": 633, + "execution_count": 684, "id": "c9773a4a", "metadata": { "tags": [] @@ -2913,7 +2891,7 @@ }, { "cell_type": "code", - "execution_count": 634, + "execution_count": 685, "id": "745486b5", "metadata": {}, "outputs": [], @@ -2933,7 +2911,7 @@ }, { "cell_type": "code", - "execution_count": 635, + "execution_count": 686, "id": "c4431bbf", "metadata": {}, "outputs": [], @@ -2962,7 +2940,7 @@ }, { "cell_type": "code", - "execution_count": 636, + "execution_count": 687, "id": "bebeecab", "metadata": {}, "outputs": [], @@ -2990,7 +2968,7 @@ }, { "cell_type": "code", - "execution_count": 637, + "execution_count": 688, "id": "36e2a2b1-6711-4266-95d8-d2aebd52e199", "metadata": { "tags": [] @@ -3019,7 +2997,7 @@ }, { "cell_type": "code", - "execution_count": 638, + "execution_count": 689, "id": "f70bcd33-b499-4e2b-953e-94d1ed96c10a", "metadata": { "tags": [] @@ -3029,9 +3007,6 @@ "name": "stderr", "output_type": "stream", "text": [ - "INFO:sagemaker.image_uris:image_uri is not presented, retrieving image_uri based on instance_type, framework etc.\n", - "WARNING:sagemaker.estimator:No finished training job found associated with this estimator. Please make sure this estimator is only used for building workflow config\n", - "WARNING:sagemaker.estimator:No finished training job found associated with this estimator. Please make sure this estimator is only used for building workflow config\n", "INFO:sagemaker.image_uris:image_uri is not presented, retrieving image_uri based on instance_type, framework etc.\n" ] }, @@ -3050,9 +3025,6 @@ "INFO:sagemaker.processing:Uploaded None to s3://mlschool/session3-pipeline/code/09fea667a5ab7c37a068f22c00762d0b/sourcedir.tar.gz\n", "INFO:sagemaker.processing:runproc.sh uploaded to s3://mlschool/session3-pipeline/code/2c207c809cb0e0e9a1d77e5247f961f9/runproc.sh\n", "WARNING:sagemaker.workflow._utils:Popping out 'CertifyForMarketplace' from the pipeline definition since it will be overridden in pipeline execution time.\n", - "INFO:sagemaker.image_uris:image_uri is not presented, retrieving image_uri based on instance_type, framework etc.\n", - "WARNING:sagemaker.estimator:No finished training job found associated with this estimator. Please make sure this estimator is only used for building workflow config\n", - "WARNING:sagemaker.estimator:No finished training job found associated with this estimator. Please make sure this estimator is only used for building workflow config\n", "INFO:sagemaker.image_uris:image_uri is not presented, retrieving image_uri based on instance_type, framework etc.\n" ] }, @@ -3068,13 +3040,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "INFO:sagemaker.processing:Uploaded None to s3://mlschool/session3-pipeline/code/09fea667a5ab7c37a068f22c00762d0b/sourcedir.tar.gz\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ + "INFO:sagemaker.processing:Uploaded None to s3://mlschool/session3-pipeline/code/09fea667a5ab7c37a068f22c00762d0b/sourcedir.tar.gz\n", "INFO:sagemaker.processing:runproc.sh uploaded to s3://mlschool/session3-pipeline/code/2c207c809cb0e0e9a1d77e5247f961f9/runproc.sh\n" ] }, @@ -3082,16 +3048,16 @@ "data": { "text/plain": [ "{'PipelineArn': 'arn:aws:sagemaker:us-east-1:325223348818:pipeline/session3-pipeline',\n", - " 'ResponseMetadata': {'RequestId': 'db9b4844-9210-4700-8a1c-1a32ef0c2251',\n", + " 'ResponseMetadata': {'RequestId': 'be91a772-a26a-4c1f-a98a-424951e6889a',\n", " 'HTTPStatusCode': 200,\n", - " 'HTTPHeaders': {'x-amzn-requestid': 'db9b4844-9210-4700-8a1c-1a32ef0c2251',\n", + " 'HTTPHeaders': {'x-amzn-requestid': 'be91a772-a26a-4c1f-a98a-424951e6889a',\n", " 'content-type': 'application/x-amz-json-1.1',\n", " 'content-length': '85',\n", - " 'date': 'Thu, 26 Oct 2023 18:43:05 GMT'},\n", + " 'date': 'Fri, 27 Oct 2023 14:38:43 GMT'},\n", " 'RetryAttempts': 0}}" ] }, - "execution_count": 638, + "execution_count": 689, "metadata": {}, "output_type": "execute_result" } @@ -3138,21 +3104,10 @@ }, { "cell_type": "code", - "execution_count": 639, + "execution_count": 690, "id": "f3b4126e", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "_PipelineExecution(arn='arn:aws:sagemaker:us-east-1:325223348818:pipeline/session3-pipeline/execution/bbkxqpvtxyxu', sagemaker_session=)" - ] - }, - "execution_count": 639, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "%%script false --no-raise-error\n", "\n", @@ -3199,7 +3154,7 @@ }, { "cell_type": "code", - "execution_count": 512, + "execution_count": 691, "id": "befd5ad3", "metadata": {}, "outputs": [], @@ -3224,7 +3179,7 @@ }, { "cell_type": "code", - "execution_count": 513, + "execution_count": 692, "id": "87437a26-e9ea-4866-9dc3-630444c0fb46", "metadata": { "tags": [] @@ -3234,14 +3189,14 @@ "data": { "text/plain": [ "{'ModelPackageGroupName': 'penguins',\n", - " 'ModelPackageVersion': 67,\n", - " 'ModelPackageArn': 'arn:aws:sagemaker:us-east-1:325223348818:model-package/penguins/67',\n", - " 'CreationTime': datetime.datetime(2023, 10, 17, 17, 7, 1, 325000, tzinfo=tzlocal()),\n", + " 'ModelPackageVersion': 74,\n", + " 'ModelPackageArn': 'arn:aws:sagemaker:us-east-1:325223348818:model-package/penguins/74',\n", + " 'CreationTime': datetime.datetime(2023, 10, 26, 14, 52, 37, 773000, tzinfo=tzlocal()),\n", " 'ModelPackageStatus': 'Completed',\n", " 'ModelApprovalStatus': 'Approved'}" ] }, - "execution_count": 513, + "execution_count": 692, "metadata": {}, "output_type": "execute_result" } @@ -3272,7 +3227,7 @@ }, { "cell_type": "code", - "execution_count": 514, + "execution_count": 693, "id": "dee516e9", "metadata": {}, "outputs": [], @@ -3308,7 +3263,7 @@ }, { "cell_type": "code", - "execution_count": 515, + "execution_count": 694, "id": "7c8852d5-818a-406c-944d-30bf6de90288", "metadata": { "tags": [] @@ -3339,7 +3294,7 @@ }, { "cell_type": "code", - "execution_count": 516, + "execution_count": 695, "id": "ba7da291", "metadata": {}, "outputs": [], @@ -3361,7 +3316,7 @@ }, { "cell_type": "code", - "execution_count": 517, + "execution_count": 696, "id": "0817a25e-8224-4911-830b-d659e7458b4a", "metadata": { "tags": [] @@ -3410,7 +3365,7 @@ }, { "cell_type": "code", - "execution_count": 518, + "execution_count": 697, "id": "6b32c3a4-312e-473c-a217-33606f77d1e9", "metadata": { "tags": [] @@ -3472,7 +3427,7 @@ }, { "cell_type": "code", - "execution_count": 519, + "execution_count": 698, "id": "e2d61d5c", "metadata": { "tags": [] @@ -3605,7 +3560,7 @@ }, { "cell_type": "code", - "execution_count": 520, + "execution_count": 699, "id": "33893ef2", "metadata": { "tags": [] @@ -3767,7 +3722,7 @@ }, { "cell_type": "code", - "execution_count": 521, + "execution_count": 700, "id": "48c69002", "metadata": { "tags": [] @@ -3876,7 +3831,7 @@ }, { "cell_type": "code", - "execution_count": 522, + "execution_count": 701, "id": "741b8402", "metadata": { "tags": [] @@ -3955,7 +3910,7 @@ }, { "cell_type": "code", - "execution_count": 523, + "execution_count": 702, "id": "53ea0ccf", "metadata": {}, "outputs": [], @@ -3981,7 +3936,7 @@ }, { "cell_type": "code", - "execution_count": 524, + "execution_count": 703, "id": "11a0effd", "metadata": {}, "outputs": [], @@ -4008,7 +3963,7 @@ }, { "cell_type": "code", - "execution_count": 525, + "execution_count": 704, "id": "5d7a5926", "metadata": {}, "outputs": [], @@ -4033,7 +3988,7 @@ }, { "cell_type": "code", - "execution_count": 526, + "execution_count": 705, "id": "157b8858", "metadata": { "tags": [] @@ -4062,7 +4017,7 @@ }, { "cell_type": "code", - "execution_count": 527, + "execution_count": 706, "id": "aefe580a", "metadata": {}, "outputs": [], @@ -4080,7 +4035,7 @@ }, { "cell_type": "code", - "execution_count": 528, + "execution_count": 707, "id": "f84d2cd5", "metadata": { "tags": [] @@ -4135,7 +4090,7 @@ }, { "cell_type": "code", - "execution_count": 529, + "execution_count": 708, "id": "b9712905-9fe3-4148-ae6d-05b0a48e742e", "metadata": { "tags": [] @@ -4162,7 +4117,7 @@ }, { "cell_type": "code", - "execution_count": 530, + "execution_count": 709, "id": "bad9f51d", "metadata": { "tags": [] @@ -4172,9 +4127,6 @@ "name": "stderr", "output_type": "stream", "text": [ - "INFO:sagemaker.image_uris:image_uri is not presented, retrieving image_uri based on instance_type, framework etc.\n", - "WARNING:sagemaker.estimator:No finished training job found associated with this estimator. Please make sure this estimator is only used for building workflow config\n", - "WARNING:sagemaker.estimator:No finished training job found associated with this estimator. Please make sure this estimator is only used for building workflow config\n", "INFO:sagemaker.image_uris:image_uri is not presented, retrieving image_uri based on instance_type, framework etc.\n" ] }, @@ -4199,9 +4151,6 @@ "name": "stderr", "output_type": "stream", "text": [ - "INFO:sagemaker.image_uris:image_uri is not presented, retrieving image_uri based on instance_type, framework etc.\n", - "WARNING:sagemaker.estimator:No finished training job found associated with this estimator. Please make sure this estimator is only used for building workflow config\n", - "WARNING:sagemaker.estimator:No finished training job found associated with this estimator. Please make sure this estimator is only used for building workflow config\n", "INFO:sagemaker.image_uris:image_uri is not presented, retrieving image_uri based on instance_type, framework etc.\n" ] }, @@ -4209,6 +4158,7 @@ "name": "stdout", "output_type": "stream", "text": [ + "Using provided s3_resource\n", "Using provided s3_resource\n" ] }, @@ -4220,27 +4170,20 @@ "INFO:sagemaker.processing:runproc.sh uploaded to s3://mlschool/session4-pipeline/code/2c207c809cb0e0e9a1d77e5247f961f9/runproc.sh\n" ] }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Using provided s3_resource\n" - ] - }, { "data": { "text/plain": [ "{'PipelineArn': 'arn:aws:sagemaker:us-east-1:325223348818:pipeline/session4-pipeline',\n", - " 'ResponseMetadata': {'RequestId': '510d5be0-0a1a-4daa-997a-12ac7b4f8e0b',\n", + " 'ResponseMetadata': {'RequestId': '2cd65edc-9bad-4b67-a1d2-aa22698d6a39',\n", " 'HTTPStatusCode': 200,\n", - " 'HTTPHeaders': {'x-amzn-requestid': '510d5be0-0a1a-4daa-997a-12ac7b4f8e0b',\n", + " 'HTTPHeaders': {'x-amzn-requestid': '2cd65edc-9bad-4b67-a1d2-aa22698d6a39',\n", " 'content-type': 'application/x-amz-json-1.1',\n", " 'content-length': '85',\n", - " 'date': 'Thu, 26 Oct 2023 18:24:38 GMT'},\n", + " 'date': 'Fri, 27 Oct 2023 14:38:46 GMT'},\n", " 'RetryAttempts': 0}}" ] }, - "execution_count": 530, + "execution_count": 709, "metadata": {}, "output_type": "execute_result" } @@ -4287,7 +4230,7 @@ }, { "cell_type": "code", - "execution_count": 531, + "execution_count": 710, "id": "20dfbd97", "metadata": {}, "outputs": [], @@ -4318,7 +4261,7 @@ }, { "cell_type": "code", - "execution_count": 348, + "execution_count": 744, "id": "998314a3", "metadata": {}, "outputs": [ @@ -4385,7 +4328,7 @@ " \"InitialInstanceCount\": 1,\n", " \"VariantName\": \"AllTraffic\",\n", " }],\n", - " \n", + "\n", " # We can enable Data Capture to record the inputs and outputs\n", " # of the endpoint to use them later for monitoring the model. \n", " DataCaptureConfig={\n", @@ -4445,7 +4388,7 @@ }, { "cell_type": "code", - "execution_count": 536, + "execution_count": 745, "id": "4ad4f1f2", "metadata": { "tags": [] @@ -4455,11 +4398,14 @@ "name": "stdout", "output_type": "stream", "text": [ - "Role lambda-deployment-role already exists.\n" + "Role \"lambda-deployment-role\" created with ARN \"arn:aws:iam::325223348818:role/lambda-deployment-role\".\n" ] } ], "source": [ + "#| code: true\n", + "#| output: false\n", + "\n", "lambda_role_name = \"lambda-deployment-role\"\n", "lambda_role_arn = None\n", "\n", @@ -4491,15 +4437,15 @@ " )\n", "\n", " iam_client.attach_role_policy(\n", - " RoleName=lambda_role_name,\n", " PolicyArn=\"arn:aws:iam::aws:policy/AmazonSageMakerFullAccess\",\n", + " RoleName=lambda_role_name,\n", " )\n", - " \n", + "\n", " print(f'Role \"{lambda_role_name}\" created with ARN \"{lambda_role_arn}\".')\n", "except iam_client.exceptions.EntityAlreadyExistsException:\n", - " print(f\"Role {lambda_role_name} already exists.\")\n", " response = iam_client.get_role(RoleName=lambda_role_name)\n", - " lambda_role_arn = response[\"Role\"][\"Arn\"]" + " lambda_role_arn = response[\"Role\"][\"Arn\"]\n", + " print(f'Role \"{lambda_role_name}\" already exists with ARN \"{lambda_role_arn}\".')\n" ] }, { @@ -4512,7 +4458,7 @@ }, { "cell_type": "code", - "execution_count": 350, + "execution_count": 747, "id": "ad8c8019", "metadata": { "tags": [] @@ -4521,36 +4467,35 @@ { "data": { "text/plain": [ - "{'ResponseMetadata': {'RequestId': 'a6e915cb-e440-4ecd-94bb-458139388602',\n", - " 'HTTPStatusCode': 200,\n", - " 'HTTPHeaders': {'date': 'Tue, 24 Oct 2023 18:28:36 GMT',\n", + "{'ResponseMetadata': {'RequestId': '57179d72-6fc2-49cc-9326-cb87bd63bda1',\n", + " 'HTTPStatusCode': 201,\n", + " 'HTTPHeaders': {'date': 'Fri, 27 Oct 2023 16:01:42 GMT',\n", " 'content-type': 'application/json',\n", - " 'content-length': '1428',\n", + " 'content-length': '1421',\n", " 'connection': 'keep-alive',\n", - " 'x-amzn-requestid': 'a6e915cb-e440-4ecd-94bb-458139388602'},\n", + " 'x-amzn-requestid': '57179d72-6fc2-49cc-9326-cb87bd63bda1'},\n", " 'RetryAttempts': 0},\n", " 'FunctionName': 'deploy_fn',\n", " 'FunctionArn': 'arn:aws:lambda:us-east-1:325223348818:function:deploy_fn',\n", " 'Runtime': 'python3.11',\n", " 'Role': 'arn:aws:iam::325223348818:role/lambda-deployment-role',\n", " 'Handler': 'lambda.lambda_handler',\n", - " 'CodeSize': 3202,\n", + " 'CodeSize': 3194,\n", " 'Description': '',\n", " 'Timeout': 600,\n", " 'MemorySize': 128,\n", - " 'LastModified': '2023-10-24T18:28:36.000+0000',\n", - " 'CodeSha256': 'gTB7D5GxQS4xUk99eaZAfIFv2GPHZ6s2D+aNyzOy19Q=',\n", + " 'LastModified': '2023-10-27T16:01:42.544+0000',\n", + " 'CodeSha256': 'IkCkE0e46WsdhSUEPRlsqEH/6nHhU5laPpgn308D30k=',\n", " 'Version': '$LATEST',\n", " 'Environment': {'Variables': {'ROLE': 'arn:aws:iam::325223348818:role/service-role/AmazonSageMaker-ExecutionRole-20230312T160501',\n", " 'DATA_CAPTURE_DESTINATION': 's3://mlschool/penguins/monitoring/data-capture',\n", " 'ENDPOINT': 'penguins-endpoint'}},\n", " 'TracingConfig': {'Mode': 'PassThrough'},\n", - " 'RevisionId': '175878c6-9ff3-47b1-b1a3-b5df361b9fc9',\n", + " 'RevisionId': '516fef1e-871b-4a52-81e2-a421f3547ec9',\n", " 'Layers': [],\n", - " 'State': 'Active',\n", - " 'LastUpdateStatus': 'InProgress',\n", - " 'LastUpdateStatusReason': 'The function is being created.',\n", - " 'LastUpdateStatusReasonCode': 'Creating',\n", + " 'State': 'Pending',\n", + " 'StateReason': 'The function is being created.',\n", + " 'StateReasonCode': 'Creating',\n", " 'PackageType': 'Zip',\n", " 'Architectures': ['x86_64'],\n", " 'EphemeralStorage': {'Size': 512},\n", @@ -4558,7 +4503,7 @@ " 'RuntimeVersionConfig': {'RuntimeVersionArn': 'arn:aws:lambda:us-east-1::runtime:6cf63f1a78b5c5e19617d6b4b111370fdbda415ea91bdfdc5aacef9fee76b64a'}}" ] }, - "execution_count": 350, + "execution_count": 747, "metadata": {}, "output_type": "execute_result" } @@ -4606,7 +4551,7 @@ }, { "cell_type": "code", - "execution_count": 351, + "execution_count": 748, "id": "27ce7cc5", "metadata": {}, "outputs": [], @@ -4633,7 +4578,7 @@ }, { "cell_type": "code", - "execution_count": 352, + "execution_count": 749, "id": "2a878179", "metadata": {}, "outputs": [], @@ -4657,7 +4602,7 @@ }, { "cell_type": "code", - "execution_count": 353, + "execution_count": 750, "id": "dc714a97", "metadata": { "tags": [] @@ -4685,18 +4630,10 @@ }, { "cell_type": "code", - "execution_count": 354, + "execution_count": 751, "id": "d74be86b", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Function \"deploy_fn\" already has permissions.\n" - ] - } - ], + "outputs": [], "source": [ "lambda_client = boto3.client(\"lambda\")\n", "try:\n", @@ -4723,7 +4660,7 @@ }, { "cell_type": "code", - "execution_count": 355, + "execution_count": 718, "id": "3cc966fb-b611-417f-a8b8-0c5d2f95252c", "metadata": { "tags": [] @@ -4786,7 +4723,7 @@ }, { "cell_type": "code", - "execution_count": 356, + "execution_count": 719, "id": "8c3e851a-2416-4a0b-b8a1-c483cde3d776", "metadata": { "tags": [] @@ -4848,7 +4785,7 @@ }, { "cell_type": "code", - "execution_count": 357, + "execution_count": 720, "id": "2bb846d0", "metadata": {}, "outputs": [], @@ -4872,7 +4809,7 @@ }, { "cell_type": "code", - "execution_count": 358, + "execution_count": 721, "id": "0b80bcab-d2c5-437c-a1c8-8eea208c0e29", "metadata": { "tags": [] @@ -4936,7 +4873,7 @@ }, { "cell_type": "code", - "execution_count": 359, + "execution_count": 722, "id": "8194b462", "metadata": {}, "outputs": [ @@ -4972,7 +4909,7 @@ }, { "cell_type": "code", - "execution_count": 360, + "execution_count": 723, "id": "bf6aa4f0", "metadata": {}, "outputs": [], @@ -5006,7 +4943,7 @@ }, { "cell_type": "code", - "execution_count": 361, + "execution_count": 724, "id": "1987a788-de7a-4f60-ac8d-819d9ffcdf8e", "metadata": { "tags": [] @@ -5052,7 +4989,7 @@ }, { "cell_type": "code", - "execution_count": 362, + "execution_count": 725, "id": "9aa3a284-8763-4000-a263-70314b530652", "metadata": { "tags": [] @@ -5116,7 +5053,7 @@ }, { "cell_type": "code", - "execution_count": 363, + "execution_count": 726, "id": "a773f134-ac2f-4dba-976e-9b7f0b384b6e", "metadata": { "tags": [] @@ -5176,7 +5113,7 @@ }, { "cell_type": "code", - "execution_count": 364, + "execution_count": 727, "id": "7056a009-91c0-4955-90dd-b90ef8cab149", "metadata": { "tags": [] @@ -5218,7 +5155,7 @@ }, { "cell_type": "code", - "execution_count": 365, + "execution_count": 728, "id": "bacaa9c6-22b0-48df-b138-95b6422fe834", "metadata": { "tags": [] @@ -5252,24 +5189,31 @@ }, { "cell_type": "code", - "execution_count": 366, + "execution_count": 729, "id": "4da5e453-acd8-47a0-a39f-264d05dd93d0", "metadata": { "tags": [] }, "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Using provided s3_resource\n" + ] + }, { "name": "stderr", "output_type": "stream", "text": [ - "INFO:sagemaker.image_uris:image_uri is not presented, retrieving image_uri based on instance_type, framework etc.\n" + "INFO:sagemaker.image_uris:image_uri is not presented, retrieving image_uri based on instance_type, framework etc.\n", + "INFO:sagemaker.processing:Uploaded None to s3://mlschool/session5-pipeline/code/09fea667a5ab7c37a068f22c00762d0b/sourcedir.tar.gz\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ - "Using provided s3_resource\n", "Using provided s3_resource\n" ] }, @@ -5277,15 +5221,16 @@ "name": "stderr", "output_type": "stream", "text": [ - "INFO:sagemaker.processing:Uploaded None to s3://mlschool/session5-pipeline/code/09fea667a5ab7c37a068f22c00762d0b/sourcedir.tar.gz\n", "INFO:sagemaker.processing:runproc.sh uploaded to s3://mlschool/session5-pipeline/code/2c207c809cb0e0e9a1d77e5247f961f9/runproc.sh\n", - "WARNING:sagemaker.workflow._utils:Popping out 'CertifyForMarketplace' from the pipeline definition since it will be overridden in pipeline execution time.\n" + "WARNING:sagemaker.workflow._utils:Popping out 'CertifyForMarketplace' from the pipeline definition since it will be overridden in pipeline execution time.\n", + "INFO:sagemaker.image_uris:image_uri is not presented, retrieving image_uri based on instance_type, framework etc.\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ + "Using provided s3_resource\n", "Using provided s3_resource\n" ] }, @@ -5293,32 +5238,24 @@ "name": "stderr", "output_type": "stream", "text": [ - "INFO:sagemaker.image_uris:image_uri is not presented, retrieving image_uri based on instance_type, framework etc.\n", "INFO:sagemaker.processing:Uploaded None to s3://mlschool/session5-pipeline/code/09fea667a5ab7c37a068f22c00762d0b/sourcedir.tar.gz\n", "INFO:sagemaker.processing:runproc.sh uploaded to s3://mlschool/session5-pipeline/code/2c207c809cb0e0e9a1d77e5247f961f9/runproc.sh\n" ] }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Using provided s3_resource\n" - ] - }, { "data": { "text/plain": [ "{'PipelineArn': 'arn:aws:sagemaker:us-east-1:325223348818:pipeline/session5-pipeline',\n", - " 'ResponseMetadata': {'RequestId': '450e9e6e-9ec9-40de-bda6-be8564981011',\n", + " 'ResponseMetadata': {'RequestId': 'e104a5af-2148-4ab4-85b3-af898d3bd315',\n", " 'HTTPStatusCode': 200,\n", - " 'HTTPHeaders': {'x-amzn-requestid': '450e9e6e-9ec9-40de-bda6-be8564981011',\n", + " 'HTTPHeaders': {'x-amzn-requestid': 'e104a5af-2148-4ab4-85b3-af898d3bd315',\n", " 'content-type': 'application/x-amz-json-1.1',\n", " 'content-length': '85',\n", - " 'date': 'Tue, 24 Oct 2023 18:28:40 GMT'},\n", + " 'date': 'Fri, 27 Oct 2023 14:38:52 GMT'},\n", " 'RetryAttempts': 0}}" ] }, - "execution_count": 366, + "execution_count": 729, "metadata": {}, "output_type": "execute_result" } @@ -5366,23 +5303,23 @@ }, { "cell_type": "code", - "execution_count": 367, + "execution_count": 739, "id": "10ba9909", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "_PipelineExecution(arn='arn:aws:sagemaker:us-east-1:325223348818:pipeline/session5-pipeline/execution/a8jrffhsgbcm', sagemaker_session=)" + "_PipelineExecution(arn='arn:aws:sagemaker:us-east-1:325223348818:pipeline/session5-pipeline/execution/ifgn9itt6qcy', sagemaker_session=)" ] }, - "execution_count": 367, + "execution_count": 739, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "%%script false --no-raise-error\n", + "# %%script false --no-raise-error\n", "\n", "#| eval: false\n", "#| code: true\n", @@ -5405,7 +5342,7 @@ }, { "cell_type": "code", - "execution_count": 406, + "execution_count": 752, "id": "42daa82b", "metadata": {}, "outputs": [ @@ -5467,7 +5404,7 @@ }, { "cell_type": "code", - "execution_count": 405, + "execution_count": 753, "id": "898d9626", "metadata": {}, "outputs": [ @@ -5508,7 +5445,7 @@ }, { "cell_type": "code", - "execution_count": 370, + "execution_count": 754, "id": "2df52332", "metadata": {}, "outputs": [ @@ -5568,7 +5505,7 @@ }, { "cell_type": "code", - "execution_count": 393, + "execution_count": 755, "id": "c658bad0", "metadata": {}, "outputs": [], @@ -5607,7 +5544,7 @@ }, { "cell_type": "code", - "execution_count": 394, + "execution_count": 756, "id": "3f35e8db-24d7-4d4b-9264-78ee5070cf27", "metadata": { "tags": [] @@ -5669,17 +5606,17 @@ }, { "cell_type": "code", - "execution_count": 395, + "execution_count": 757, "id": "bb999995", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "'s3://mlschool/penguins/monitoring/groundtruth/2023/10/24/19/3952.jsonl'" + "'s3://mlschool/penguins/monitoring/groundtruth/2023/10/27/17/0816.jsonl'" ] }, - "execution_count": 395, + "execution_count": 757, "metadata": {}, "output_type": "execute_result" } @@ -5725,7 +5662,7 @@ }, { "cell_type": "code", - "execution_count": 380, + "execution_count": 758, "id": "da145ba1-4966-4dab-8a73-281db364cbc7", "metadata": { "tags": [] @@ -5864,7 +5801,7 @@ }, { "cell_type": "code", - "execution_count": 381, + "execution_count": 759, "id": "cc119422-2e85-4e8c-86cd-6d59e353d09d", "metadata": { "tags": [] @@ -5884,7 +5821,7 @@ }, { "cell_type": "code", - "execution_count": 382, + "execution_count": 760, "id": "083b0bd0-4035-43fe-9b2c-946b12a5e266", "metadata": { "tags": [] @@ -5927,7 +5864,7 @@ }, { "cell_type": "code", - "execution_count": 383, + "execution_count": 761, "id": "96e5c0c1-7e40-47df-8f40-1d891db13875", "metadata": { "tags": [] @@ -5977,14 +5914,14 @@ }, { "cell_type": "code", - "execution_count": 385, + "execution_count": null, "id": "15caf9e1-97fc-4379-893b-6062d4bd876e", "metadata": { "tags": [] }, "outputs": [], "source": [ - "%%script false --no-raise-error\n", + "# %%script false --no-raise-error\n", "#| code: true\n", "#| output: false\n", "#| eval: false\n", @@ -6020,7 +5957,7 @@ }, { "cell_type": "code", - "execution_count": 401, + "execution_count": 781, "id": "2c04fdd4-cc03-496c-a0a1-405854505c46", "metadata": { "tags": [] @@ -6184,7 +6121,7 @@ }, { "cell_type": "code", - "execution_count": 403, + "execution_count": 783, "id": "bb74dc04-54a1-4a3f-854f-4877f7f0b4a1", "metadata": { "tags": [] @@ -6195,7 +6132,7 @@ "output_type": "stream", "text": [ "Monitoring schedule deleted.\n", - "Monitoring schedule deleted.\n" + "There's no ModelQuality Monitoring Schedule.\n" ] } ], @@ -6229,19 +6166,10 @@ }, { "cell_type": "code", - "execution_count": 404, + "execution_count": null, "id": "9eabe84e", "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "INFO:sagemaker:Deleting endpoint configuration with name: penguins-endpoint-config-1024190416\n", - "INFO:sagemaker:Deleting endpoint with name: penguins-endpoint\n" - ] - } - ], + "outputs": [], "source": [ "%%script false --no-raise-error\n", "#| eval: false\n", diff --git a/program/index.qmd b/program/index.qmd index b487df0..3004711 100644 --- a/program/index.qmd +++ b/program/index.qmd @@ -4,7 +4,7 @@ listing: contents: posts sort: "date desc" type: default - categories: true + categories: false --- Welcome to the program! @@ -73,8 +73,14 @@ Welcome to the program! * The 3 strategies to keep your models working despite data distribution shifts * Understanding SageMaker’s Transform Step, QualityCheck Step, Transform Jobs, and Monitoring Jobs - -## Table of Contents - -* [Configuration Setup](setup.qmd) -* [Cohort Notebook](cohort.ipynb) \ No newline at end of file +#### Session 6 - Continual Learning And Testing in Production + +* The importance of Continual Learning and why every company wants to to do it +* 3 challenges when implementing Continual Learning +* A 4-step plan to implement Continual Learning +* How to determine what data to use to retrain a model +* A 3-step progressive plan to decide how frequently you should retrain your models +* The differences between training from scratch and incremental training +* An introduction to Testing in Production +* 5 strategies to test models in production: A/B testing, shadow deployments, canary releases, interleaving experiments, and multi-armed bandits +* Highlights from the program diff --git a/program/project.qmd b/program/project.qmd new file mode 100644 index 0000000..d34e295 --- /dev/null +++ b/program/project.qmd @@ -0,0 +1,28 @@ +--- +title: "Class Project" +--- + +The goal of this project is to build a training pipeline to preprocess, train, evaluate, and register a machine learning model. + +You'll start from the template pipeline that we discussed during the program and make the necessary changes to it. Before making any changes, ensure you can run the pipeline from Session 4 without issues. + +The project has three different levels of complexity. Pick the one that you feel most comfortable tackling first. + +## Simple complexity +We want to replace the Penguins dataset with a different classification problem. Feel free to use any dataset you like. If you don't have any ideas, here are three options you can choose from: + +1. [Iris flowers](https://archive.ics.uci.edu/dataset/53/iris) dataset - This is a multi-class classification problem where you'll predict the flower species given the measurements of iris flowers. +2. [Adult income](https://archive.ics.uci.edu/dataset/2/adult) dataset - This is a binary classification problem where you'll predict whether the income of a person exceeds $50,000/yr based on census data. +3. [Banknote authentication](https://archive.ics.uci.edu/dataset/267/banknote+authentication) dataset - This is a binary classification problem where you'll predict whether a given banknote is authentic given the measures from a photograph. + +Start with the pipeline from Session 4 and modify the preprocessing, training, and evaluation scripts to use the new dataset. + +## Intermediate complexity +We want to replace TensorFlow with PyTorch in the pipeline we built in Session 4. Everything else will stay the same, except the framework to train the model. + +Start with the pipeline from Session 4 and modify the training and evaluation scripts to train and evaluate the model using PyTorch. Notice you'll need to use a [PyTorch estimator](https://sagemaker.readthedocs.io/en/stable/frameworks/pytorch/sagemaker.pytorch.html) to configure the Training Step and a [PyTorch processor](https://sagemaker.readthedocs.io/en/stable/frameworks/pytorch/sagemaker.pytorch.html#pytorch-processor) to configure the evaluation step. + +## Advanced complexity +At this stage, we want to combine replacing the Penguins dataset with replacing TensorFlow with PyTorch in the pipeline. + +Start with the pipeline from Session 4 and make the necessary changes described in the simple and intermediate complexity sections. \ No newline at end of file diff --git a/program/setup.qmd b/program/setup.qmd index 8a6b6ab..bba8279 100644 --- a/program/setup.qmd +++ b/program/setup.qmd @@ -4,7 +4,7 @@ listing: contents: posts sort: "date desc" type: default - categories: true + categories: false --- Here are the steps you need to follow to set up the project: diff --git a/program/sidebar.yml b/program/sidebar.yml index 71e914e..c3b3d58 100644 --- a/program/sidebar.yml +++ b/program/sidebar.yml @@ -3,4 +3,5 @@ website: contents: - index.qmd - setup.qmd + - project.qmd - cohort.ipynb \ No newline at end of file