{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# SageMaker model deployment as CI/CD pipeline\n", "This notebook demonstrates how to use SageMaker Project template for CI/CD model deployment. You are going to implement:
\n", "1. Load the data for the iris multi-class classification problem
\n", "2. Use a SageMaker built-in estimator [XGBoost](https://docs.aws.amazon.com/sagemaker/latest/dg/xgboost.html) to train the model on the dataset
\n", "3. Create a [SageMaker pipeline](https://docs.aws.amazon.com/sagemaker/latest/dg/pipelines.html) to train and register the model
\n", "4. Select the latest model package from the model package group and set the status to `Approved` and launch the model deployment CI/CD pipeline" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Load packages and get environment configuration " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "if False:\n", " !pip install -U sagemaker" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%matplotlib inline\n", "import pandas as pd\n", "import numpy as np\n", "import sagemaker\n", "import json\n", "import boto3\n", "from sagemaker import get_execution_role\n", "import sagemaker.session\n", "from sklearn.model_selection import train_test_split\n", "from sklearn import datasets\n", "\n", "sm = boto3.client(\"sagemaker\")\n", "ssm = boto3.client(\"ssm\")\n", "\n", "def get_environment(project_name, ssm_params):\n", " r = sm.describe_domain(\n", " DomainId=sm.describe_project(\n", " ProjectName=project_name\n", " )[\"CreatedBy\"][\"DomainId\"]\n", " )\n", " del r[\"ResponseMetadata\"]\n", " del r[\"CreationTime\"]\n", " del r[\"LastModifiedTime\"]\n", " r = {**r, **r[\"DefaultUserSettings\"]}\n", " del r[\"DefaultUserSettings\"]\n", "\n", " i = {\n", " **r,\n", " **{t[\"Key\"]:t[\"Value\"] \n", " for t in sm.list_tags(ResourceArn=r[\"DomainArn\"])[\"Tags\"] \n", " if t[\"Key\"] in [\"EnvironmentName\", \"EnvironmentType\"]}\n", " }\n", "\n", " for p in ssm_params:\n", " try:\n", " i[p[\"VariableName\"]] = ssm.get_parameter(Name=f\"{i['EnvironmentName']}-{i['EnvironmentType']}-{p['ParameterName']}\")[\"Parameter\"][\"Value\"]\n", " except:\n", " i[p[\"VariableName\"]] = \"\"\n", "\n", " return i\n", "\n", "def get_session(region, default_bucket):\n", " \"\"\"Gets the sagemaker session based on the region.\n", "\n", " Args:\n", " region: the aws region to start the session\n", " default_bucket: the bucket to use for storing the artifacts\n", "\n", " Returns:\n", " sagemaker.session.Session instance\n", " \"\"\"\n", "\n", " boto_session = boto3.Session(region_name=region)\n", "\n", " sagemaker_client = boto_session.client(\"sagemaker\")\n", " runtime_client = boto_session.client(\"sagemaker-runtime\")\n", " return sagemaker.session.Session(\n", " boto_session=boto_session,\n", " sagemaker_client=sagemaker_client,\n", " sagemaker_runtime_client=runtime_client,\n", " default_bucket=default_bucket,\n", " )" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
💡 Get environment variables \n", "\n", "Set the `project_name` to the name of the current SageMaker project.\n", "Various environment data is loaded and shown:\n", "
" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Set to the specific SageMaker project name\n", "project_name = \n", "\n", "# Dynamically load environmental SSM parameters - provide the list of the variables to load from SSM parameter store\n", "ssm_parameters = [\n", " {\"VariableName\":\"DataBucketName\", \"ParameterName\":\"data-bucket-name\"},\n", " {\"VariableName\":\"ModelBucketName\", \"ParameterName\":\"model-bucket-name\"},\n", " {\"VariableName\":\"S3VPCEId\", \"ParameterName\":\"s3-vpce-id\"},\n", " {\"VariableName\":\"S3KmsKeyId\", \"ParameterName\":\"kms-s3-key-arn\"},\n", " {\"VariableName\":\"EbsKmsKeyArn\", \"ParameterName\":\"kms-ebs-key-arn\"},\n", " {\"VariableName\":\"PipelineExecutionRole\", \"ParameterName\":\"sm-pipeline-execution-role-arn\"},\n", " {\"VariableName\":\"ModelExecutionRole\", \"ParameterName\":\"sm-model-execution-role-name\"},\n", " {\"VariableName\":\"StackSetExecutionRole\", \"ParameterName\":\"stackset-execution-role-name\"},\n", " {\"VariableName\":\"StackSetAdministrationRole\", \"ParameterName\":\"stackset-administration-role-arn\"},\n", " {\"VariableName\":\"StagingAccountList\", \"ParameterName\":\"staging-account-list\"},\n", " {\"VariableName\":\"ProdAccountList\", \"ParameterName\":\"production-account-list\"},\n", " {\"VariableName\":\"EnvTypeStagingName\", \"ParameterName\":\"env-type-staging-name\"},\n", " {\"VariableName\":\"EnvTypeProdName\", \"ParameterName\":\"env-type-prod-name\"},\n", "]\n", "\n", "env_data = get_environment(project_name=project_name, ssm_params=ssm_parameters)\n", "print(f\"Environment data:\\n{json.dumps(env_data, indent=2)}\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Create SageMaker session\n", "sagemaker_session = get_session(boto3.Session().region_name, env_data[\"DataBucketName\"])\n", "\n", "region = boto3.Session().region_name\n", "pipeline_role = env_data[\"PipelineExecutionRole\"]\n", "processing_role = env_data[\"ExecutionRole\"]\n", "model_execution_role = env_data[\"ModelExecutionRole\"]\n", "training_role = env_data[\"ExecutionRole\"]\n", "data_bucket = sagemaker_session.default_bucket()\n", "model_bucket = env_data[\"ModelBucketName\"]\n", "\n", "print(f\"SageMaker version: {sagemaker.__version__}\")\n", "print(f\"Region: {region}\")\n", "print(f\"Pipeline execution role: {pipeline_role}\")\n", "print(f\"Processing role: {processing_role}\")\n", "print(f\"Training role: {training_role}\")\n", "print(f\"Model execution role: {model_execution_role}\")\n", "print(f\"Pipeline data bucket: {data_bucket}\")\n", "print(f\"Pipeline model bucket: {model_bucket}\")\n", "\n", "\n", "project_id = sm.describe_project(ProjectName=project_name)['ProjectId']\n", "# The model package group name must be the same as specified at project creation time in ModelPackageGroupName parameter\n", "model_package_group_name = f\"{project_name}-{project_id}\"\n", "print(f\"Model package group name: {model_package_group_name}\")\n", "\n", "assert(len(project_name) <= 15 ) # the project name should not have more than 15 chars\n", "\n", "# Prefix for S3 objects\n", "prefix=f\"{project_name}-{project_id}\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Setup the network config\n", "You must provide the network configuration such as subnet ids and security group ids for SageMaker training and register model jobs. The security controls in the SageMaker execution role IAM policy prevents starting any SageMaker job without VPC attachment." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from sagemaker.network import NetworkConfig\n", "\n", "network_config = NetworkConfig(\n", " enable_network_isolation=False, \n", " security_group_ids=env_data[\"SecurityGroups\"],\n", " subnets=env_data[\"SubnetIds\"],\n", " encrypt_inter_container_traffic=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Load the dataset\n", "\n", "### Load from scikit-learn\n", "Load the [iris dataset](https://scikit-learn.org/stable/modules/generated/sklearn.datasets.load_iris.html) from `sklearn` module. The iris dataset is a classic and very easy multi-class classification dataset." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "iris = datasets.load_iris()\n", "dataset = np.insert(iris.data, 0, iris.target, axis=1)\n", "\n", "df = pd.DataFrame(data=dataset, columns=['iris_id'] + iris.feature_names)\n", "df['species'] = df['iris_id'].map(lambda x: 'setosa' if x == 0 else 'versicolor' if x == 1 else 'virginica')\n", "\n", "df.head()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Upload the dataset to an S3 bucket" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "X=iris.data\n", "y=iris.target\n", "\n", "# Split the dataset into train and test parts\n", "X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33, random_state=42, stratify=y)\n", "yX_train = np.column_stack((y_train, X_train))\n", "yX_test = np.column_stack((y_test, X_test))\n", "np.savetxt(\"iris_train.csv\", yX_train, delimiter=\",\", fmt='%0.3f')\n", "np.savetxt(\"iris_test.csv\", yX_test, delimiter=\",\", fmt='%0.3f')\n", "\n", "# Upload the dataset to an S3 bucket\n", "input_train = sagemaker_session.upload_data(path='iris_train.csv', key_prefix=f'{prefix}/datasets/iris/data')\n", "input_test = sagemaker_session.upload_data(path='iris_test.csv', key_prefix=f'{prefix}/datasets/iris/data')\n", "\n", "print(input_train)\n", "print(input_test)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Create the ML Pipeline" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Pipeline input parameters" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from sagemaker.workflow.parameters import (\n", " ParameterInteger,\n", " ParameterString,\n", ")\n", "\n", "training_instance_type = ParameterString(\n", " name=\"TrainingInstanceType\",\n", " default_value=\"ml.m5.xlarge\"\n", ")\n", "training_instance_count = ParameterInteger(\n", " name=\"TrainingInstanceCount\",\n", " default_value=1\n", ")\n", "input_train_data = ParameterString(\n", " name=\"InputDataTrain\",\n", " default_value=input_train,\n", ")\n", "input_test_data = ParameterString(\n", " name=\"InputDataTest\",\n", " default_value=input_test,\n", ")\n", "model_approval_status = ParameterString(\n", " name=\"ModelApprovalStatus\", default_value=\"PendingManualApproval\"\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Setup an estimator that will run the training process" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from sagemaker.estimator import Estimator\n", "import time\n", "\n", "base_job_prefix = f\"{prefix}/iris-{time.strftime('%Y-%m-%d-%H-%M-%S')}\"\n", "model_path = f\"s3://{model_bucket}/{base_job_prefix}\"\n", "\n", "image_uri = sagemaker.image_uris.retrieve(\n", " framework=\"xgboost\", \n", " region=region, \n", " version=\"1.0-1\", \n", " py_version=\"py3\", \n", " instance_type=training_instance_type,\n", ")\n", "xgb_train = Estimator(\n", " image_uri=image_uri,\n", " instance_type=training_instance_type,\n", " instance_count=training_instance_count,\n", " output_path=model_path,\n", " base_job_name=f\"{base_job_prefix}/train\",\n", " sagemaker_session=sagemaker_session,\n", " role=training_role,\n", " subnets=network_config.subnets,\n", " security_group_ids=network_config.security_group_ids,\n", " encrypt_inter_container_traffic=True,\n", " enable_network_isolation=False,\n", " volume_kms_key=env_data[\"EbsKmsKeyArn\"],\n", " output_kms_key=env_data[\"S3KmsKeyId\"]\n", ")\n", "xgb_train.set_hyperparameters(\n", " eta=0.1,\n", " max_depth=10,\n", " gamma=4,\n", " num_class=len(np.unique(y)),\n", " alpha=10,\n", " min_child_weight=6,\n", " silent=0,\n", " objective='multi:softmax',\n", " num_round=30\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Training step" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from sagemaker.inputs import TrainingInput\n", "from sagemaker.workflow.steps import TrainingStep\n", "\n", "step_train = TrainingStep(\n", " name=\"IrisTrain\",\n", " estimator=xgb_train,\n", " inputs={\n", " \"train\": TrainingInput(s3_data=input_train_data, content_type=\"text/csv\"),\n", " \"validation\": TrainingInput(s3_data=input_test_data, content_type=\"text/csv\"\n", " )\n", " },\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Model register step" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "vpc_config = {\n", " \"Subnets\":network_config.subnets,\n", " \"SecurityGroupIds\":network_config.security_group_ids\n", "}" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from sagemaker.workflow.step_collections import RegisterModel\n", "\n", "# NOTE: model_approval_status is not available as arg in service dsl currently\n", "step_register = RegisterModel(\n", " name=\"IrisRegisterModel\",\n", " estimator=xgb_train,\n", " model_data=step_train.properties.ModelArtifacts.S3ModelArtifacts,\n", " content_types=[\"text/csv\"],\n", " response_types=[\"text/csv\"],\n", " inference_instances=[\"ml.t2.medium\", \"ml.m5.xlarge\"],\n", " transform_instances=[\"ml.m5.xlarge\"],\n", " model_package_group_name=model_package_group_name,\n", " approval_status=model_approval_status,\n", " vpc_config_override=vpc_config\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Create a pipeline\n", "For sake of simplicity we limit the pipeline to train and register steps only. For real-life production example you might create a pipeline with data processing, training, model evaluation, and conditional model register steps. This extended example is covered by `MLOps Model Build Train` SageMaker project template." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from botocore.exceptions import ClientError, ValidationError\n", "from sagemaker.workflow.pipeline import Pipeline\n", "\n", "\n", "pipeline_name = f\"{prefix}-IrisPipeline\"\n", "\n", "pipeline = Pipeline(\n", " name=pipeline_name,\n", " parameters=[\n", " training_instance_type,\n", " training_instance_count, \n", " input_train_data,\n", " model_approval_status,\n", " input_test_data\n", " ],\n", " steps=[step_train, step_register],\n", " sagemaker_session=sagemaker_session,\n", ")\n", "\n", "response = pipeline.upsert(role_arn=pipeline_role)\n", "\n", "pipeline_arn = response[\"PipelineArn\"]\n", "sm.add_tags(\n", " ResourceArn=pipeline_arn,\n", " Tags=[\n", " {'Key': 'sagemaker:project-name', 'Value': project_name },\n", " {'Key': 'sagemaker:project-id', 'Value': project_id },\n", " {'Key': 'EnvironmentName', 'Value': env_data['EnvironmentName'] },\n", " {'Key': 'EnvironmentType', 'Value': env_data['EnvironmentType'] },\n", " ]\n", ")\n", "\n", "print(response)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Execute the pipeline" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "execution = pipeline.start()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "execution.describe()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Wait till the completion of the pipeline" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "execution.wait()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Finally, approve the model to launch the model deployment process" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# list all model packages and select the latest one\n", "model_packages = []\n", "\n", "for p in sm.get_paginator('list_model_packages').paginate(\n", " ModelPackageGroupName=model_package_group_name,\n", " SortBy=\"CreationTime\",\n", " SortOrder=\"Descending\",\n", " ):\n", " model_packages.extend(p[\"ModelPackageSummaryList\"])\n", "\n", "if len(model_packages) == 0:\n", " raise Exception(f\"No model package is found for {model_package_group_name} model package group\")\n", " \n", "latest_model_package_arn = model_packages[0][\"ModelPackageArn\"]\n", "print(latest_model_package_arn)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The following statement sets the `ModelApprovalStatus` for the model package to `Approved`. The model package state change will launch the EventBridge rule and the rule will launch the CodePipeline CI/CD pipeline with model deployment." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "model_package_update_response = sm.update_model_package(\n", " ModelPackageArn=latest_model_package_arn,\n", " ModelApprovalStatus=\"Approved\",\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The model deployment CI/CD pipeline will perform the followign actions:
\n", "1. Create a SageMaker endpoint in staging account (or `*-staging` endpoint in the current account in case of single-account deployment)
\n", "2. Run the test script on the staging endpoint
\n", "3. Wait until the test result is manually approved in [AWS CodePipeline console](https://console.aws.amazon.com/codesuite/codepipeline)
\n", "4. Create a SageMaker endpoint in the production account (or `*-prod` endpoint in the current account in case of single-account deployment)
\n", "\n", "After successful completion of the CI/CD pipeline, you will see two endpoints in status `InService` in SageMaker Studio Components->Endpoints widget." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### CI/CD pipeline execution\n", "You can follow up the execution of the model deployment pipeline including the stages and actions" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "cp = boto3.client(\"codepipeline\")\n", "\n", "code_pipeline_name = f\"sagemaker-{project_name}-{project_id}-modeldeploy\"\n", "\n", "r = cp.get_pipeline_state(name=code_pipeline_name)\n", "\n", "r" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Wait about 15 minutes until the staging endpoint is deployed and the pipeline stops at the manual approval stage:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import time\n", "from IPython.core.display import display, HTML\n", "\n", "print(f\"waiting till the pipeline stops at the manual approval stage\")\n", "\n", "while len([a for a in [s for s in cp.get_pipeline_state(\n", " name=code_pipeline_name\n", " )[\"stageStates\"] if s[\"stageName\"] == \"DeployModelStaging\"][0][\"actionStates\"]\n", " if a[\"actionName\"]==\"ApproveStagingDeployment\" and a.get(\"latestExecution\") and a.get(\"latestExecution\")[\"status\"]==\"InProgress\"])==0:\n", " print(\"waiting...\")\n", " time.sleep(20)\n", "\n", "print(f\"staging deployment completed.\")\n", "\n", "display(\n", " HTML(\n", " 'Please approve the manual step in AWS CodePipeline'.format(\n", " code_pipeline_name, region)\n", " )\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Click on the link ^^^ above ^^^ to approve the production deployment." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "After completion of the previous code snippet, you will have a staging endpoint deployed in the SageMaker environment in the staging account or in the same account in the case of single-account setup. Approve the production deployment by clicking on the provided link above and approving the CodePipeline manual approval stage.
\n", "The model deployment pipeline continues and deploys the production endpoint into the production account.\n", "You can check the status and details of the SageMaker endpoint in the `Component and registries`->`Endpoints` widget:\n", "\n", "![endpoints](img/endpoints.png)\n", "\n", "Please keep in mind, that you can see the deployed staging and production SageMaker endpoints in Studio in the case of single-account deployment only. If you deploy the model to different staging and production accounts, you have to log into the AWS console in the corresponding account." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Production deployment\n", "Wait another 15 minutes until the model has been deployed to production." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print(f\"waiting for production endpoint deployment\")\n", "\n", "while len([a for a in [s for s in cp.get_pipeline_state(\n", " name=code_pipeline_name\n", " )[\"stageStates\"] if s[\"stageName\"] == \"DeployModelProd\"][0][\"actionStates\"]\n", " if a[\"actionName\"]==\"DeployProd\" and a.get(\"latestExecution\") and a.get(\"latestExecution\")[\"status\"]==\"Succeeded\"])==0:\n", " print(\"waiting...\")\n", " time.sleep(20)\n", "\n", "print(f\"production deployment completed.\")\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Clean up\n", "After you have finished testing and experimenting with model deployment, you should clean up the provisioned resources to avoid charges for the SageMaker inference instances.
\n", "The code in this section deletes the SageMaker staging and production endpoints. The corresponding CloudFormation stack set instances and stack sets are also deleted." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Delete CloudFormation stack sets\n", "This will delete provisioned SageMaker endpoints and associated resoures." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import time\n", "\n", "cf = boto3.client(\"cloudformation\")\n", "\n", "for ss in [\n", " f\"sagemaker-{project_name}-{project_id}-deploy-{env_data['EnvTypeStagingName']}\",\n", " f\"sagemaker-{project_name}-{project_id}-deploy-{env_data['EnvTypeProdName']}\"\n", " ]:\n", " accounts = [a[\"Account\"] for a in cf.list_stack_instances(StackSetName=ss)[\"Summaries\"]]\n", " print(f\"delete stack set instances for {ss} stack set for the accounts {accounts}\")\n", " r = cf.delete_stack_instances(\n", " StackSetName=ss,\n", " Accounts=accounts,\n", " Regions=[boto3.session.Session().region_name],\n", " RetainStacks=False,\n", " )\n", " print(r)\n", "\n", " time.sleep(180)\n", "\n", " print(f\"delete stack set {ss}\")\n", " r = cf.delete_stack_set(\n", " StackSetName=ss\n", " )" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Delete SageMaker project\n", "This will delete the associated CloudFormation stack and CodeCommit repository" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print(f\"Deleting project {project_name}:{sm.delete_project(ProjectName=project_name)}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Delete project S3 bucket\n", "This will remove all files and S3 bucket" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "!aws s3 rb s3://sm-mlops-cp-{project_name}-{project_id} --force" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Release resources" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%html\n", "\n", "

Shutting down your kernel for this notebook to release resources.

\n", "\n", " \n", "" ] } ], "metadata": { "instance_type": "ml.t3.medium", "kernelspec": { "display_name": "Python 3 (Data Science)", "language": "python", "name": "python3__SAGEMAKER_INTERNAL__arn:aws:sagemaker:us-east-1:081325390199:image/datascience-1.0" }, "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.7.10" }, "metadata": { "interpreter": { "hash": "ac2eaa0ea0ebeafcc7822e65e46aa9d4f966f30b695406963e145ea4a91cd4fc" } } }, "nbformat": 4, "nbformat_minor": 4 }