{ "cells": [ { "cell_type": "markdown", "id": "6e3fcd46-c757-4a47-9b60-8ef02213d1c0", "metadata": {}, "source": [ "# Basic Pipeline for Batch Inference using Low-code Experience for SageMaker Pipelines" ] }, { "cell_type": "markdown", "id": "84fab569-dd25-475e-b522-25572dcc403c", "metadata": {}, "source": [ "---\n", "\n", "This notebook's CI test result for us-west-2 is as follows. CI test results in other regions can be found at the end of the notebook.\n", "\n", "![This us-west-2 badge failed to load. Check your device's internet connectivity, otherwise the service is currently unavailable](https://prod.us-west-2.tcx-beacon.docs.aws.dev/sagemaker-nb/us-west-2/sagemaker-pipelines|step-decorator|batch-examples|basic-pipeline-batch-inference.ipynb)\n", "\n", "---" ] }, { "cell_type": "markdown", "id": "81b8c1ee-15df-4ae0-b312-169303297121", "metadata": { "tags": [] }, "source": [ "This notebook shows the example of orchestrating jobs for model building and batch inference using low-code experience for SageMaker Pipelines, utilizing @step decorator. We build an automated model building pipeline for a classification problem predicting if a breast cancer diagnostic data indicates benign or malignant tumor. The model building pipeline includes preprocessing step, training step, evaluation step, and register model step. We also create another pipeline to perform batch inference." ] }, { "cell_type": "markdown", "id": "39407027-ca13-471d-a9ed-1a9d96033a1d", "metadata": {}, "source": [ "## Dataset\n", "\n", "We use breast cancer Wisconsin diagnostic dataset:\n", ">\n", "> Wolberg,William, Mangasarian,Olvi, Street,Nick, and Street,W.. (1995). Breast Cancer Wisconsin (Diagnostic). UCI Machine Learning Repository. https://doi.org/10.24432/C5DW2B.\n", "\n", "The dataset can be downloaded from UCI Machine Learning Repository: https://archive.ics.uci.edu/dataset/17/breast+cancer+wisconsin+diagnostic\n" ] }, { "cell_type": "markdown", "id": "ab3c4e37-83c7-4d31-b780-b52a861ed976", "metadata": {}, "source": [ "## Notebook Preparation" ] }, { "cell_type": "markdown", "id": "cb299115-5dfa-41e4-91a8-129afc41f32b", "metadata": { "tags": [] }, "source": [ "In this section we install and prepare the library dependencies we need to use in this notebook, as well as initiate our sagemaker session." ] }, { "cell_type": "code", "execution_count": null, "id": "f35e335f-4084-4fba-98a3-8d8caaf618a1", "metadata": { "scrolled": true, "tags": [] }, "outputs": [], "source": [ "%pip install -U boto3" ] }, { "cell_type": "code", "execution_count": null, "id": "654e20a7-e2e4-4660-aeaa-d242ea4b3d54", "metadata": { "tags": [] }, "outputs": [], "source": [ "import os\n", "import boto3" ] }, { "cell_type": "code", "execution_count": null, "id": "7291fbcf-4e06-4568-9fe7-71e60bf20cc5", "metadata": { "scrolled": true, "tags": [], "ExecuteTime": { "start_time": "2023-11-14T15:46:27.679087Z", "end_time": "2023-11-14T15:46:30.269723Z" } }, "outputs": [], "source": [ "%pip install -r ./requirements.txt" ] }, { "cell_type": "markdown", "id": "d3fce868-f125-4c88-82d9-47baed6d6d33", "metadata": {}, "source": [ "We can use configuration file `config.yaml` to set default values of the infrastructure such as instance type, and dependencies to run the pipeline. We use environment variable \"SAGEMAKER_USER_CONFIG_OVERRIDE\" to set the path to configuration file." ] }, { "cell_type": "code", "execution_count": null, "id": "c35ed1ec-122b-42a8-adba-e01e8a4cbeac", "metadata": { "tags": [] }, "outputs": [], "source": [ "# Set path to config file\n", "os.environ[\"SAGEMAKER_USER_CONFIG_OVERRIDE\"] = os.getcwd()" ] }, { "cell_type": "code", "execution_count": null, "id": "68ca5730-9864-475f-a7bc-927933579c4d", "metadata": { "tags": [] }, "outputs": [], "source": [ "import sagemaker\n", "from sagemaker.workflow.function_step import step\n", "from sagemaker.workflow.parameters import ParameterString\n", "\n", "sagemaker_session = sagemaker.session.Session()\n", "role = sagemaker.get_execution_role()\n", "bucket = sagemaker_session.default_bucket()\n", "region = sagemaker_session.boto_region_name" ] }, { "cell_type": "markdown", "id": "8f67e3e1-0aa8-4e40-a1a2-ee9331fea993", "metadata": {}, "source": [ "## Define variables and pipeline parameters" ] }, { "cell_type": "code", "execution_count": null, "id": "8f05d160-48c6-4c54-9b7d-e345c2f3e8ee", "metadata": { "tags": [] }, "outputs": [], "source": [ "# Location of our dataset\n", "input_path = f\"s3://sagemaker-example-files-prod-{region}/datasets/tabular/breast_cancer/wdbc.csv\"" ] }, { "cell_type": "code", "execution_count": null, "id": "91dee7ce-5ed9-4a51-b8f5-838a4ee6e0eb", "metadata": { "tags": [] }, "outputs": [], "source": [ "pipeline_name = \"lowcode-breast-cancer-xgb\"\n", "model_package_group_name = \"lowcode-breast-cancer-xgb\"" ] }, { "cell_type": "code", "execution_count": null, "id": "772f5e24-d5a8-49f4-b8d9-b9513ca73731", "metadata": { "tags": [] }, "outputs": [], "source": [ "instance_type = ParameterString(name=\"TrainingInstanceType\", default_value=\"ml.m5.xlarge\")\n", "model_approval_status = ParameterString(\n", " name=\"ModelApprovalStatus\", default_value=\"PendingManualApproval\"\n", ")" ] }, { "cell_type": "markdown", "id": "22f67b95-bc16-4563-ae6f-d9140d727420", "metadata": {}, "source": [ "## Preprocessing Step" ] }, { "cell_type": "markdown", "id": "32ae759b-e3c9-4860-8b7a-d6561ac5107b", "metadata": {}, "source": [ "The breast cancer Wisconsin dataset contains column `id` which we do not use for training. The second column `diagnosis` is class label, and the label is represented using 'M' for Malignant class, and 'B' for Benign class. \n", "\n", "In the preprocessing step, we drop the column `id`, then split the dataset into three distinct sets: train, validation, and test set.\n", "\n", "Note that `keep_alive_period_in_seconds` parameter in @step decorator indicates how many seconds we want to keep the instance alive, waiting to be reused for the next pipeline step execution. Setting this parameter speeds up the pipeline execution because we reduce the launching of new instances to execute pipeline steps." ] }, { "cell_type": "code", "execution_count": null, "id": "edf939b6-2509-4321-9722-b5ff9b5ebc43", "metadata": { "tags": [] }, "outputs": [], "source": [ "random_state = 2023\n", "label_column = \"diagnosis\"\n", "\n", "feature_names = [\n", " \"id\",\n", " \"diagnosis\",\n", " \"radius_mean\",\n", " \"texture_mean\",\n", " \"perimeter_mean\",\n", " \"area_mean\",\n", " \"smoothness_mean\",\n", " \"compactness_mean\",\n", " \"concavity_mean\",\n", " \"concave points_mean\",\n", " \"symmetry_mean\",\n", " \"fractal_dimension_mean\",\n", " \"radius_se\",\n", " \"texture_se\",\n", " \"perimeter_se\",\n", " \"area_se\",\n", " \"smoothness_se\",\n", " \"compactness_se\",\n", " \"concavity_se\",\n", " \"concave points_se\",\n", " \"symmetry_se\",\n", " \"fractal_dimension_se\",\n", " \"radius_worst\",\n", " \"texture_worst\",\n", " \"perimeter_worst\",\n", " \"area_worst\",\n", " \"smoothness_worst\",\n", " \"compactness_worst\",\n", " \"concavity_worst\",\n", " \"concave points_worst\",\n", " \"symmetry_worst\",\n", " \"fractal_dimension_worst\",\n", "]\n", "\n", "\n", "@step(\n", " name=\"data-preprocessing\",\n", " instance_type=instance_type,\n", " keep_alive_period_in_seconds=300,\n", ")\n", "def preprocess(raw_data_s3_path: str, output_prefix: str) -> tuple:\n", " import pandas as pd\n", " from sklearn.model_selection import train_test_split\n", "\n", " df = pd.read_csv(raw_data_s3_path, header=None, names=feature_names)\n", " df.drop(columns=\"id\", inplace=True)\n", "\n", " train_df, test_df = train_test_split(df, test_size=0.2, stratify=df[label_column])\n", " validation_df, test_df = train_test_split(\n", " test_df, test_size=0.5, stratify=test_df[label_column]\n", " )\n", " train_df.reset_index(inplace=True, drop=True)\n", " validation_df.reset_index(inplace=True, drop=True)\n", " test_df.reset_index(inplace=True, drop=True)\n", "\n", " train_s3_path = f\"s3://{bucket}/{output_prefix}/train.csv\"\n", " val_s3_path = f\"s3://{bucket}/{output_prefix}/val.csv\"\n", " test_s3_path = f\"s3://{bucket}/{output_prefix}/test.csv\"\n", "\n", " train_df.to_csv(train_s3_path, index=False)\n", " validation_df.to_csv(val_s3_path, index=False)\n", " test_df.to_csv(test_s3_path, index=False)\n", "\n", " return train_s3_path, val_s3_path, test_s3_path" ] }, { "cell_type": "markdown", "id": "baaa5d21-6e0f-44f5-b05e-d695f279246d", "metadata": {}, "source": [ "## Training Step" ] }, { "cell_type": "markdown", "id": "4f3566f4-aadd-4f8b-a22e-b6185015ec4f", "metadata": {}, "source": [ "We train an XGBoost model in this training step, using @step-decorated function with the S3 path of training and validation set, along with XGBoost hyperparameters. The S3 paths for both training and validation set is coming from the output of the previous step." ] }, { "cell_type": "code", "execution_count": null, "id": "7858e86b-1acf-4029-bf3b-acd45a4859b8", "metadata": { "tags": [] }, "outputs": [], "source": [ "use_gpu = False\n", "param = dict(\n", " objective=\"binary:logistic\",\n", " max_depth=5,\n", " eta=0.2,\n", " gamma=4,\n", " min_child_weight=6,\n", " subsample=0.7,\n", " tree_method=\"gpu_hist\" if use_gpu else \"hist\", # Use GPU accelerated algorithm\n", ")\n", "num_round = 50\n", "\n", "\n", "@step(\n", " name=\"model-training\",\n", " instance_type=instance_type,\n", " keep_alive_period_in_seconds=300,\n", ")\n", "def train(\n", " train_s3_path: str,\n", " validation_s3_path: str,\n", " param: dict = param,\n", " num_round: int = num_round,\n", "):\n", " import pandas as pd\n", " from xgboost import XGBClassifier\n", "\n", " # read data files from S3\n", " train_df = pd.read_csv(train_s3_path)\n", " validation_df = pd.read_csv(validation_s3_path)\n", "\n", " # create dataframe and label series\n", " y_train = (train_df.pop(label_column) == \"M\").astype(\"int\")\n", " y_validation = (validation_df.pop(label_column) == \"M\").astype(\"int\")\n", "\n", " xgb = XGBClassifier(n_estimators=num_round, **param)\n", " xgb.fit(\n", " train_df,\n", " y_train,\n", " eval_set=[(validation_df, y_validation)],\n", " early_stopping_rounds=5,\n", " )\n", "\n", " return xgb" ] }, { "cell_type": "markdown", "id": "0a32b578-6f21-424d-b323-e46e3a04f42a", "metadata": {}, "source": [ "## Evaluation Step" ] }, { "cell_type": "markdown", "id": "60a4dbe9-15bd-49c0-83cc-7196b59f9908", "metadata": {}, "source": [ "In this step, we create a @step-decorated function evaluate the trained XGBoost model on the test dataset." ] }, { "cell_type": "code", "execution_count": null, "id": "e9425da9-e973-4ff5-ac8d-522b32a20c4d", "metadata": { "tags": [] }, "outputs": [], "source": [ "@step(\n", " name=\"model-evaluation\",\n", " instance_type=instance_type,\n", " keep_alive_period_in_seconds=300,\n", ")\n", "def evaluate(model, test_s3_path: str) -> dict:\n", " import json\n", " import numpy as np\n", " import pandas as pd\n", " from sklearn.metrics import (\n", " accuracy_score,\n", " auc,\n", " confusion_matrix,\n", " f1_score,\n", " precision_score,\n", " recall_score,\n", " roc_curve,\n", " )\n", "\n", " test_df = pd.read_csv(test_s3_path)\n", " y_test = (test_df.pop(label_column) == \"M\").astype(\"int\")\n", "\n", " prediction_probabilities = model.predict_proba(test_df)\n", " predictions = np.argmax(prediction_probabilities, axis=1)\n", "\n", " acc = accuracy_score(y_test, predictions)\n", " precision = precision_score(y_test, predictions, zero_division=1)\n", " recall = recall_score(y_test, predictions)\n", " f1 = f1_score(y_test, predictions)\n", " conf_matrix = confusion_matrix(y_test, predictions)\n", " fpr, tpr, _ = roc_curve(y_test, prediction_probabilities[:, 1])\n", " auc_value = auc(fpr, tpr)\n", "\n", " report_dict = {\n", " \"binary_classification_metrics\": {\n", " \"accuracy\": {\"value\": acc, \"standard_deviation\": \"NaN\"},\n", " \"f1\": {\"value\": f1, \"standard_deviation\": \"NaN\"},\n", " \"precision\": {\"value\": precision, \"standard_deviation\": \"NaN\"},\n", " \"recall\": {\"value\": recall, \"standard_deviation\": \"NaN\"},\n", " \"confusion_matrix\": {\n", " \"0\": {\"0\": int(conf_matrix[0][0]), \"1\": int(conf_matrix[0][1])},\n", " \"1\": {\"0\": int(conf_matrix[1][0]), \"1\": int(conf_matrix[1][1])},\n", " },\n", " \"receiver_operating_characteristic_curve\": {\n", " \"false_positive_rates\": list(fpr),\n", " \"true_positive_rates\": list(tpr),\n", " },\n", " \"auc\": {\"value\": auc_value, \"standard_deviation\": \"NaN\"},\n", " },\n", " }\n", "\n", " print(f\"evaluation report: {json.dumps(report_dict, indent=2)}\")\n", " return report_dict" ] }, { "cell_type": "markdown", "id": "6b7ded00-a01d-49cf-9467-99df2dc130d3", "metadata": {}, "source": [ "## Model Registration Step" ] }, { "cell_type": "markdown", "id": "5404af5a-2718-41d3-9589-81ae2af8cb68", "metadata": {}, "source": [ "In this step, we register the trained model to Model Registry. We use `ModelBuilder` to build model artifacts for inference." ] }, { "cell_type": "code", "execution_count": null, "id": "960c0e0f-65ef-44e6-8de8-c6537bd76944", "metadata": { "tags": [] }, "outputs": [], "source": [ "@step(\n", " name=\"model-registration\",\n", " instance_type=instance_type,\n", " keep_alive_period_in_seconds=300,\n", ")\n", "def register(model, evaluation, model_approval_status, sample_data):\n", " import json\n", " import numpy as np\n", " import pandas as pd\n", " import s3fs\n", " from pathlib import Path\n", " from sagemaker import MetricsSource, ModelMetrics\n", " from sagemaker.serve.builder.model_builder import ModelBuilder\n", " from sagemaker.serve.builder.schema_builder import SchemaBuilder\n", " from sagemaker.serve.spec.inference_spec import InferenceSpec\n", " from sagemaker.utils import unique_name_from_base\n", " from xgboost import XGBClassifier\n", "\n", " class XGBoostSpec(InferenceSpec):\n", " def load(self, model_dir: str):\n", " print(model_dir)\n", " model = XGBClassifier()\n", " model.load_model(model_dir + \"/xgboost-model\")\n", " return model\n", "\n", " def invoke(self, input_object: object, model: object):\n", " prediction_probabilities = model.predict_proba(input_object)\n", " predictions = np.argmax(prediction_probabilities, axis=1)\n", " return predictions\n", "\n", " # Upload evaluation report to s3\n", " eval_file_name = unique_name_from_base(\"evaluation\")\n", " eval_report_s3_uri = (\n", " f\"s3://{bucket}/{model_package_group_name}/evaluation-report/{eval_file_name}.json\"\n", " )\n", " s3_fs = s3fs.S3FileSystem()\n", " eval_report_str = json.dumps(evaluation)\n", " with s3_fs.open(eval_report_s3_uri, \"wb\") as file:\n", " file.write(eval_report_str.encode(\"utf-8\"))\n", "\n", " # Create model_metrics as per evaluation report in s3\n", " model_metrics = ModelMetrics(\n", " model_statistics=MetricsSource(\n", " s3_uri=eval_report_s3_uri,\n", " content_type=\"application/json\",\n", " )\n", " )\n", "\n", " sample_data = pd.read_csv(sample_data, nrows=10)\n", " sample_data.pop(label_column)\n", "\n", " schema_builder = SchemaBuilder(\n", " sample_input=sample_data.to_numpy(),\n", " sample_output=model.predict(sample_data),\n", " )\n", "\n", " model_path = Path(\"/tmp/model/\")\n", " model_path.mkdir(parents=True, exist_ok=True)\n", " model.save_model(model_path / \"xgboost-model\")\n", "\n", " # Build the trained model and register it\n", " model_builder = ModelBuilder(\n", " model_path=str(model_path),\n", " inference_spec=XGBoostSpec(),\n", " schema_builder=schema_builder,\n", " role_arn=role,\n", " s3_model_data_url=f\"s3://{bucket}/{model_package_group_name}/model-artifacts\",\n", " )\n", " model_package = model_builder.build().register(\n", " model_package_group_name=model_package_group_name,\n", " approval_status=model_approval_status,\n", " model_metrics=model_metrics,\n", " )\n", "\n", " print(f\"Registered Model Package ARN: {model_package.model_package_arn}\")\n", " return model_package.model_package_arn" ] }, { "cell_type": "markdown", "id": "d491a2a3-9554-4b16-84e9-8c4994017e39", "metadata": {}, "source": [ "## Putting everything together: creating the Pipeline and running the pipeline execution" ] }, { "cell_type": "markdown", "id": "5b554985-f418-4847-806a-16b9dd007a85", "metadata": {}, "source": [ "We connect all defined pipeline `@step` functions into a multi-step pipeline. Then, we submit and execute the pipeline." ] }, { "cell_type": "code", "execution_count": null, "id": "92dde82c-1522-44b1-aa16-957cd38bc6af", "metadata": { "tags": [] }, "outputs": [], "source": [ "from sagemaker.workflow.pipeline import Pipeline\n", "\n", "delayed_data = preprocess(\n", " raw_data_s3_path=input_path,\n", " output_prefix=f\"{pipeline_name}/dataset\",\n", ")\n", "delayed_model = train(train_s3_path=delayed_data[0], validation_s3_path=delayed_data[1])\n", "delayed_evaluation = evaluate(model=delayed_model, test_s3_path=delayed_data[2])\n", "delayed_register = register(\n", " model=delayed_model,\n", " evaluation=delayed_evaluation,\n", " model_approval_status=model_approval_status,\n", " sample_data=delayed_data[2],\n", ")\n", "\n", "pipeline = Pipeline(\n", " name=pipeline_name,\n", " parameters=[\n", " instance_type,\n", " model_approval_status,\n", " ],\n", " steps=[\n", " delayed_register,\n", " ],\n", ")" ] }, { "cell_type": "code", "execution_count": null, "id": "300b1476-1456-4d4c-acf7-a7ddafc5cb98", "metadata": { "scrolled": true, "tags": [] }, "outputs": [], "source": [ "pipeline.upsert(role_arn=role)" ] }, { "cell_type": "code", "execution_count": null, "id": "a917217d-a18c-41aa-89ec-ff0d47ec1131", "metadata": { "tags": [] }, "outputs": [], "source": [ "execution = pipeline.start()" ] }, { "cell_type": "code", "execution_count": null, "id": "aaa0fc27-c175-4015-bf5e-dd17e3f5f8f5", "metadata": { "tags": [] }, "outputs": [], "source": [ "execution.describe()" ] }, { "cell_type": "code", "execution_count": null, "id": "ccd2c868-1690-46f1-b70c-9982f13a651d", "metadata": { "tags": [] }, "outputs": [], "source": [ "execution.wait()" ] }, { "cell_type": "code", "execution_count": null, "id": "ff889c61-27b3-47de-a608-eb08d7a67dda", "metadata": { "tags": [] }, "outputs": [], "source": [ "execution.list_steps()" ] }, { "cell_type": "markdown", "id": "cc3153f4-a72c-4c0b-867c-a2fd2ac4b474", "metadata": { "tags": [] }, "source": [ "## Using Trained Model in Model Registry to perform Batch Inference" ] }, { "cell_type": "markdown", "id": "69730f67-5941-42fc-8863-bf1aea156198", "metadata": {}, "source": [ "The pipeline execution automatically runs preprocessing, training, evaluation, and registration of trained model to the model registry. The model versions in model registry are candidate models to actual deployment, and we can approve a model version in model registry after the pipeline execution completes. \n", "\n", "The code snippet is the example code to get the latest model version in the model registry and update its status to `\"Approved\"`." ] }, { "cell_type": "code", "execution_count": null, "id": "6f599b9c-5c2b-45f6-9496-7eda51836054", "metadata": {}, "outputs": [], "source": [ "sm = boto3.client(\"sagemaker\")\n", "\n", "# Get the latest model\n", "model_package_arn = execution.result(step_name=\"model-registration\")\n", "model_package_update_input_dict = {\n", " \"ModelPackageArn\": model_package_arn,\n", " \"ModelApprovalStatus\": \"Approved\",\n", "}\n", "model_package_update_response = sm.update_model_package(**model_package_update_input_dict)" ] }, { "cell_type": "markdown", "id": "ae7d0384-c169-455f-8ce1-9246036c826f", "metadata": {}, "source": [ "We can use `describe_model_package()` to see the details of the approved model package." ] }, { "cell_type": "code", "execution_count": null, "id": "a7caf4ae-741b-4dcd-ae69-5519b8a230d7", "metadata": {}, "outputs": [], "source": [ "sm.describe_model_package(ModelPackageName=model_package_arn)" ] }, { "cell_type": "markdown", "id": "128f09e3-2106-4290-93e1-1cc39eca119f", "metadata": {}, "source": [ "The `\"ModelDataUrl\"` inside `InferenceSpecification` indicates the location of model artifacts. We will use the model artifact to load the model for batch inference." ] }, { "cell_type": "code", "execution_count": null, "id": "4676dc2b-b838-43b5-9454-bfd609da00a6", "metadata": { "tags": [] }, "outputs": [], "source": [ "model_package = sm.describe_model_package(ModelPackageName=model_package_arn)\n", "model_artifact_s3_path = model_package[\"InferenceSpecification\"][\"Containers\"][0][\"ModelDataUrl\"]" ] }, { "cell_type": "markdown", "id": "2e2e6773-3631-4a01-ad3b-aba42344ad9b", "metadata": {}, "source": [ "We can create another pipeline for batch inference using the approved model version. In this example, we take the original dataset (with ID and labels removed) as the dataset to be predicted." ] }, { "cell_type": "code", "execution_count": null, "id": "0b77b88b-f2fc-474f-828a-6eab183f0b3b", "metadata": { "tags": [] }, "outputs": [], "source": [ "# create a dataset for batch prediction\n", "import pandas as pd\n", "\n", "batch_dataset_filename = \"wdbc_to_be_predicted.csv\"\n", "data_df = pd.read_csv(\n", " input_path,\n", " header=None,\n", " names=feature_names,\n", ")\n", "data_df = data_df.drop(\n", " data_df.columns[[0, 1]], axis=1\n", ") # Remove the first two columns (ID and Labels)\n", "data_df.to_csv(batch_dataset_filename, index=False)\n", "batch_dataset_s3_path = sagemaker_session.upload_data(\n", " path=batch_dataset_filename,\n", " key_prefix=f\"{pipeline_name}/dataset/\",\n", ")" ] }, { "cell_type": "code", "execution_count": null, "id": "a7bd9974-d8b0-42e0-a3a5-8b80262262ff", "metadata": { "tags": [] }, "outputs": [], "source": [ "inference_pipeline_name = \"lowcode-breast-cancer-xgb-inference\"" ] }, { "cell_type": "code", "execution_count": null, "id": "b2bc1b43-c2ba-415d-8955-0b7dcd5c14e7", "metadata": { "tags": [] }, "outputs": [], "source": [ "@step(\n", " name=\"batch-inference\",\n", " instance_type=instance_type,\n", " keep_alive_period_in_seconds=300,\n", ")\n", "def batch_inference(\n", " model_package_s3_path: str,\n", " dataset_s3_path: str,\n", " output_s3_path: str,\n", "):\n", " import numpy as np\n", " import pandas as pd\n", " import s3fs\n", " import tarfile\n", " from xgboost import XGBClassifier\n", "\n", " package_filename = \"serve.tar.gz\"\n", " s3_fs = s3fs.S3FileSystem()\n", " s3_fs.get_file(model_package_s3_path, package_filename)\n", " with tarfile.open(package_filename) as tar:\n", " tar.extractall(path=\".\")\n", " model = XGBClassifier()\n", " model.load_model(\"xgboost-model\")\n", "\n", " df = pd.read_csv(dataset_s3_path)\n", " prediction_probabilities = model.predict_proba(df)\n", " predictions = np.argmax(prediction_probabilities, axis=1)\n", " with s3_fs.open(output_s3_path, \"wb\") as file:\n", " file.write(\"\\n\".join(np.char.mod(\"%d\", predictions)).encode(\"utf-8\"))" ] }, { "cell_type": "code", "execution_count": null, "id": "dc689ba7-c4b6-4310-bf42-f53cf06d2cb9", "metadata": { "tags": [] }, "outputs": [], "source": [ "from sagemaker.workflow.pipeline import Pipeline\n", "\n", "delayed_inference = batch_inference(\n", " model_artifact_s3_path,\n", " batch_dataset_s3_path,\n", " f\"s3://{bucket}/{inference_pipeline_name}/output/predictions.txt\",\n", ")\n", "\n", "batch_inference_pipeline = Pipeline(\n", " name=inference_pipeline_name,\n", " parameters=[\n", " instance_type,\n", " ],\n", " steps=[\n", " delayed_inference,\n", " ],\n", ")" ] }, { "cell_type": "code", "execution_count": null, "id": "a9fb54ab-ade7-47f7-b438-7e28470d4bc2", "metadata": { "tags": [] }, "outputs": [], "source": [ "batch_inference_pipeline.upsert(role_arn=role)" ] }, { "cell_type": "code", "execution_count": null, "id": "1eeefa5b-2ccd-4d0a-a864-a8f84af33982", "metadata": { "tags": [] }, "outputs": [], "source": [ "execution = batch_inference_pipeline.start()" ] }, { "cell_type": "code", "execution_count": null, "id": "42cc4eb6-ca41-4fc5-b315-70bd6d65d680", "metadata": { "tags": [] }, "outputs": [], "source": [ "execution.describe()" ] }, { "cell_type": "code", "execution_count": null, "id": "3fda13ce-67c0-4965-97a3-3f0a1da484fa", "metadata": { "tags": [] }, "outputs": [], "source": [ "execution.wait()" ] }, { "cell_type": "code", "execution_count": null, "id": "f80421f7-b508-4daf-850f-09934343da6d", "metadata": { "tags": [] }, "outputs": [], "source": [ "execution.list_steps()" ] }, { "cell_type": "markdown", "id": "034a40be-695a-41c0-be54-cc8361a30689", "metadata": {}, "source": [ "## Clean up Resources" ] }, { "cell_type": "markdown", "id": "1121a39f-1f2c-4ec0-ba05-82005d185021", "metadata": {}, "source": [ "When you finish with the notebook, you can delete unused resources such as model package, and pipeline. Here are the example codes for clean up, you can adjust the code to follow your variable names.\n" ] }, { "cell_type": "markdown", "id": "fdfde07b-836d-4692-ba45-eef8a6feefd9", "metadata": {}, "source": [ "### Delete model package" ] }, { "cell_type": "code", "execution_count": null, "id": "97063d55-cf2b-4791-832c-338546ab0289", "metadata": { "tags": [] }, "outputs": [], "source": [ "paginator = sm.get_paginator(\"list_model_packages\")\n", "for page in paginator.paginate(ModelPackageGroupName=model_package_group_name):\n", " for model_package in page[\"ModelPackageSummaryList\"]:\n", " print(model_package[\"ModelPackageArn\"])\n", " sm.delete_model_package(ModelPackageName=model_package[\"ModelPackageArn\"])\n", "\n", "sm.delete_model_package_group(ModelPackageGroupName=model_package_group_name)" ] }, { "cell_type": "markdown", "id": "256bd14b-677e-42fe-8bd8-b4f9806e0f1c", "metadata": {}, "source": [ "### Delete pipeline" ] }, { "cell_type": "code", "execution_count": null, "id": "a526a5c2-242f-4970-b3ff-ee316bbc24dc", "metadata": {}, "outputs": [], "source": [ "pipeline.delete()\n", "batch_inference_pipeline.delete()" ] }, { "cell_type": "markdown", "id": "5c435682-766a-4cc7-b417-ae7752b309ea", "metadata": {}, "source": [ "## Notebook CI Test Results\n", "\n", "This notebook was tested in multiple regions. The test results are as follows, except for us-west-2 which is shown at the top of the notebook.\n", "\n", "\n", "![This us-east-1 badge failed to load. Check your device's internet connectivity, otherwise the service is currently unavailable](https://prod.us-west-2.tcx-beacon.docs.aws.dev/sagemaker-nb/us-east-1/sagemaker-pipelines|step-decorator|batch-examples|basic-pipeline-batch-inference.ipynb)\n", "\n", "![This us-east-2 badge failed to load. Check your device's internet connectivity, otherwise the service is currently unavailable](https://prod.us-west-2.tcx-beacon.docs.aws.dev/sagemaker-nb/us-east-2/sagemaker-pipelines|step-decorator|batch-examples|basic-pipeline-batch-inference.ipynb)\n", "\n", "![This us-west-1 badge failed to load. Check your device's internet connectivity, otherwise the service is currently unavailable](https://prod.us-west-2.tcx-beacon.docs.aws.dev/sagemaker-nb/us-west-1/sagemaker-pipelines|step-decorator|batch-examples|basic-pipeline-batch-inference.ipynb)\n", "\n", "![This ca-central-1 badge failed to load. Check your device's internet connectivity, otherwise the service is currently unavailable](https://prod.us-west-2.tcx-beacon.docs.aws.dev/sagemaker-nb/ca-central-1/sagemaker-pipelines|step-decorator|batch-examples|basic-pipeline-batch-inference.ipynb)\n", "\n", "![This sa-east-1 badge failed to load. Check your device's internet connectivity, otherwise the service is currently unavailable](https://prod.us-west-2.tcx-beacon.docs.aws.dev/sagemaker-nb/sa-east-1/sagemaker-pipelines|step-decorator|batch-examples|basic-pipeline-batch-inference.ipynb)\n", "\n", "![This eu-west-1 badge failed to load. Check your device's internet connectivity, otherwise the service is currently unavailable](https://prod.us-west-2.tcx-beacon.docs.aws.dev/sagemaker-nb/eu-west-1/sagemaker-pipelines|step-decorator|batch-examples|basic-pipeline-batch-inference.ipynb)\n", "\n", "![This eu-west-2 badge failed to load. Check your device's internet connectivity, otherwise the service is currently unavailable](https://prod.us-west-2.tcx-beacon.docs.aws.dev/sagemaker-nb/eu-west-2/sagemaker-pipelines|step-decorator|batch-examples|basic-pipeline-batch-inference.ipynb)\n", "\n", "![This eu-west-3 badge failed to load. Check your device's internet connectivity, otherwise the service is currently unavailable](https://prod.us-west-2.tcx-beacon.docs.aws.dev/sagemaker-nb/eu-west-3/sagemaker-pipelines|step-decorator|batch-examples|basic-pipeline-batch-inference.ipynb)\n", "\n", "![This eu-central-1 badge failed to load. Check your device's internet connectivity, otherwise the service is currently unavailable](https://prod.us-west-2.tcx-beacon.docs.aws.dev/sagemaker-nb/eu-central-1/sagemaker-pipelines|step-decorator|batch-examples|basic-pipeline-batch-inference.ipynb)\n", "\n", "![This eu-north-1 badge failed to load. Check your device's internet connectivity, otherwise the service is currently unavailable](https://prod.us-west-2.tcx-beacon.docs.aws.dev/sagemaker-nb/eu-north-1/sagemaker-pipelines|step-decorator|batch-examples|basic-pipeline-batch-inference.ipynb)\n", "\n", "![This ap-southeast-1 badge failed to load. Check your device's internet connectivity, otherwise the service is currently unavailable](https://prod.us-west-2.tcx-beacon.docs.aws.dev/sagemaker-nb/ap-southeast-1/sagemaker-pipelines|step-decorator|batch-examples|basic-pipeline-batch-inference.ipynb)\n", "\n", "![This ap-southeast-2 badge failed to load. Check your device's internet connectivity, otherwise the service is currently unavailable](https://prod.us-west-2.tcx-beacon.docs.aws.dev/sagemaker-nb/ap-southeast-2/sagemaker-pipelines|step-decorator|batch-examples|basic-pipeline-batch-inference.ipynb)\n", "\n", "![This ap-northeast-1 badge failed to load. Check your device's internet connectivity, otherwise the service is currently unavailable](https://prod.us-west-2.tcx-beacon.docs.aws.dev/sagemaker-nb/ap-northeast-1/sagemaker-pipelines|step-decorator|batch-examples|basic-pipeline-batch-inference.ipynb)\n", "\n", "![This ap-northeast-2 badge failed to load. Check your device's internet connectivity, otherwise the service is currently unavailable](https://prod.us-west-2.tcx-beacon.docs.aws.dev/sagemaker-nb/ap-northeast-2/sagemaker-pipelines|step-decorator|batch-examples|basic-pipeline-batch-inference.ipynb)\n", "\n", "![This ap-south-1 badge failed to load. Check your device's internet connectivity, otherwise the service is currently unavailable](https://prod.us-west-2.tcx-beacon.docs.aws.dev/sagemaker-nb/ap-south-1/sagemaker-pipelines|step-decorator|batch-examples|basic-pipeline-batch-inference.ipynb)\n" ] }, { "cell_type": "code", "execution_count": null, "id": "1285d712-42f3-4658-94f6-bcea132a4db2", "metadata": {}, "outputs": [], "source": [] } ], "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 }, { "_defaultOrder": 57, "_isFastLaunch": false, "category": "Accelerated computing", "gpuNum": 0, "hideHardwareSpecs": false, "memoryGiB": 32, "name": "ml.trn1.2xlarge", "vcpuNum": 8 }, { "_defaultOrder": 58, "_isFastLaunch": false, "category": "Accelerated computing", "gpuNum": 0, "hideHardwareSpecs": false, "memoryGiB": 512, "name": "ml.trn1.32xlarge", "vcpuNum": 128 }, { "_defaultOrder": 59, "_isFastLaunch": false, "category": "Accelerated computing", "gpuNum": 0, "hideHardwareSpecs": false, "memoryGiB": 512, "name": "ml.trn1n.32xlarge", "vcpuNum": 128 } ], "instance_type": "ml.t3.medium", "kernelspec": { "name": "python3", "language": "python", "display_name": "Python 3 (ipykernel)" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.10.6" } }, "nbformat": 4, "nbformat_minor": 5 }