🚢

SageMaker SDKを利用してタイタニック号の生存者を予測してみる

2024/06/10に公開

概要

AWS の AI/ML サービスは以下の3つのレイヤーで構成されている

  • AI サービス
  • ML サービス
  • ML フレームワーク / インフラストラクチャ

MLサービスに位置し、機械学習モデルの構築、トレーニング、デプロイを簡単に行えるフルマネージドサービスとして SageMaker がある

https://aws.amazon.com/jp/sagemaker/

今回はこれを利用して、機械学習における定番チュートリアルである「タイタニック号の生存者予測」に取り組んでみる
範囲は、モデルのトレーニングからデプロイまでとする

前提条件

  • Python がインストールされていること
  • AWS CLI がインストールされていること
  • AWSアカウントを保有しており、プロファイルを作成済みであること

(optional) 仮想環境を構築

目的はホストOSのPython環境を汚さないようにすることなので不要なら省略しても問題なし

$ cd /path/to/project
$ python3 -m venv .venv
$ source .venv/bin/activate

上記以外でも rye を使うなどしてもOK

パッケージのインストール

SDK とその他必要なパッケージをインストール

$ pip install sagemaker boto3 pandas

S3バケットの生成

IaC使ってもマネージメントコンソールからでもなんでもOKなので適当にバケットを作る
バケットにはテストデータをアップロードしたり、SageMakerがトレーニングジョブによって作成したモデルアーティファクトがアップロードされたりする

データセットのダウンロード

以下からタイタニック号の生存者予測のデータセットをダウンロードする
https://www.kaggle.com/c/titanic/data

ダウンロードしたアーカイブを解凍すると以下のファイルがある

  • gender_submission.csv
  • test.csv
  • train.csv

test.csvtrain.csv をプロジェクトルートに配置する

前処理

preprocessing.py というスクリプトを作成する

preprocessing.py
import pandas as pd

def preprocess_data(input_file, output_file, target_column='Survived'):
    data = pd.read_csv(input_file)

    if target_column and target_column in data.columns:
        # NOTE: SageMakerのXGBoostは、デフォルトではCSVファイルの最初の列を目的変数として扱う
        target = data[target_column]
        data = data.drop(columns=[target_column])
        data.insert(0, target_column, target)

    data = data.drop(columns=['Name', 'Ticket'])
    data['Sex'] = data['Sex'].map({'male': 0, 'female': 1})
    data['Embarked'] = data['Embarked'].map({'C': 0, 'Q': 1, 'S': 2})
    data["Cabin"] = data["Cabin"].map(lambda x: 0 if x is None else 1)
    data.fillna(data.select_dtypes(include=['float64', 'int64']).mean(), inplace=True)
    data = data.fillna(0)

    data.to_csv(output_file, index=False, header=False)
    print(f"{output_file} has been created.")

preprocess_data('train.csv', 'train_processed.csv')
preprocess_data('test.csv', 'test_processed.csv')

これを実行すると、前処理されたデータがプロジェクトルートに出力される

$ python preprocessing.py
train_processed.csv has been created.
test_processed.csv has been created.

教師データの作成は非常に重要な作業だが、今回は精度を出すことを目的としないので内容は適当

データセットのアップロード

プロファイルはデフォルトのものが適用されるので、適当に export AWS_PROFILE=sagemaker-handson などしておく

$ aws s3 cp train_processed.csv s3://your-bucket-name
upload: ./train_processed.csv to s3://your-bucket-name/train_processed.csv

IAM ロールの作成

SDKが隠蔽しているものもあるが、SageMakerはS3,ECR,EC2など様々なサービスを呼び出している
それらのファイルやイメージなどへのアクセス許可が必要

今回は簡便のため、最小権限の設定などは行わない
こちらでは sagemaker-role っていう名前のロールを作り、AWSマネージドの AmazonSageMakerFullAccess というポリシーを当てた

モデルの訓練

さっき作ったS3バケットの名前とロールのARNを変数に設定した上で training.py というスクリプトを作る

training.py
import sagemaker
from sagemaker.estimator import Estimator
from sagemaker.inputs import TrainingInput

role = 'arn:aws:iam::************:role/sagemaker-role'

bucket_name = 'your-bucket-name'
train_data_path = f's3://{bucket_name}/train_processed.csv'
output_path = f's3://{bucket_name}/output'

sagemaker_session = sagemaker.Session()
xgboost_estimator = Estimator(
    image_uri=sagemaker.image_uris.retrieve(framework='xgboost', region=sagemaker_session.boto_region_name, version='1.2-1'),
    role=role,
    instance_count=1,
    instance_type='ml.m5.xlarge',
    output_path=f's3://{bucket_name}/output',
    sagemaker_session=sagemaker_session
)

xgboost_estimator.set_hyperparameters(
    num_round=100
)

train_input = TrainingInput(s3_data=train_data_path, content_type='csv')

xgboost_estimator.fit({'train': train_input})

これを実行する

$ python training.py
sagemaker.config INFO - Not applying SDK defaults from location: /Library/Application Support/sagemaker/config.yaml
sagemaker.config INFO - Not applying SDK defaults from location: /Users/k0kishima/Library/Application Support/sagemaker/config.yaml
INFO:sagemaker:Creating training-job with name: sagemaker-xgboost-2024-06-09-21-49-28-231
# 以下省略

実行後に、バケットに "output/sagemaker-xgboost-2024-06-09-11-45-33-292/output/model.tar.gz" のようなロケーションに出来上がったモデルアーティファクトがアップロードされる

モデルのデプロイ

ロール・前節でアップロードされたモデルアーティファクトのパス・エンドポイント名をそれぞれ指定した上で deploy.py を作る

deploy.py
import sagemaker
from sagemaker.model import Model
import boto3

role = 'arn:aws:iam::************:role/sagemaker-role'
model_artifact = 's3://your-backet-name/output/sagemaker-xgboost-2024-06-09-13-24-57-794/output/model.tar.gz'
endpoint_name = 'your-endpoint-name'

sm_client = boto3.client('sagemaker')

endpoints = sm_client.list_endpoints(NameContains=endpoint_name)
existing_endpoints = [ep for ep in endpoints['Endpoints'] if ep['EndpointName'] == endpoint_name]

if existing_endpoints:
    print(f"Endpoint {endpoint_name} already exists.")
else:
    sagemaker_session = sagemaker.Session()

    model = Model(
        model_data=model_artifact,
        image_uri=sagemaker.image_uris.retrieve(framework='xgboost', region=sagemaker_session.boto_region_name, version='1.2-1'),
        role=role,
        sagemaker_session=sagemaker_session
    )

    predictor = model.deploy(
        initial_instance_count=1,
        instance_type='ml.m5.xlarge',
        endpoint_name=endpoint_name
    )
    print(f"Endpoint {endpoint_name} has been created.")

実行するとエンドポイントが作成される

$ python deploy.py
sagemaker.config INFO - Not applying SDK defaults from location: /Library/Application Support/sagemaker/config.yaml
sagemaker.config INFO - Not applying SDK defaults from location: /Users/k0kishima/Library/Application Support/sagemaker/config.yaml
-----!Endpoint your-endpoint-name has been created.

動作確認

エンドポイントから予測結果を取得してみる

$ aws sagemaker-runtime invoke-endpoint \                                                                                                                       
  --endpoint-name your-endpoint-name \ 
  --body fileb://test_processed.csv \
  --content-type text/csv \
  output.json
{
    "ContentType": "text/csv; charset=utf-8",
    "InvokedProductionVariant": "AllTraffic"
}
$ cat output.json 
0.036303937435150146,0.19049838185310364,0.285813570022583,0.26095426082611084,0.4801110327243805,  # 以下省略

後始末

エンドポイントを削除する

エンドポイントは稼働時間に基づいて従量課金されるので削除する

  • SageMakerのダッシュボード、もしくはサイドメニューからエンドポイント一覧へアクセスする
  • 今回作成したエンドポイントを選択し、削除する

S3の削除

トレーニングジョブによりアップロードされたデータは、S3のストレージ使用量に基づいて課金されるので削除する

  • 不要ならS3バケットを消す
  • バケット自体は残すなら不要なオブジェクトは全部消す

おまけ(IaCコード)

モデルアーティファクトがすでにS3にアップロードされているなら以下でエンドポイントを作れる

main.tf
terraform {
  required_version = "~> 1.7"
  required_providers {
    aws = {
      source  = "hashicorp/aws",
      version = "5.40"
    }
  }
}

provider "aws" {
  region = "ap-northeast-1"
}

variable "container_image" {
  type = string
}

variable "model_data_url" {
  type = string
}

variable "model_name" {
  type = string
}

variable "endpoint_name" {
  type = string
}

resource "aws_iam_role" "sagemaker_execution" {
  name = "sagemaker-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        Effect = "Allow",
        Principal = {
          Service = "sagemaker.amazonaws.com"
        },
        Action = "sts:AssumeRole"
      }
    ]
  })
}

resource "aws_iam_role_policy_attachment" "this" {
  role       = aws_iam_role.sagemaker_execution.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonSageMakerFullAccess"
}

resource "aws_sagemaker_model" "this" {
  name               = var.model_name
  execution_role_arn = aws_iam_role.sagemaker_execution.arn

  primary_container {
    image         = var.container_image
    model_data_url = var.model_data_url
  }
}

resource "aws_sagemaker_endpoint_configuration" "this" {
  name = "sagemaker-endpoint-config"

  production_variants {
    variant_name           = "AllTraffic"
    model_name             = aws_sagemaker_model.this.name
    initial_instance_count = 1
    instance_type          = "ml.m5.xlarge"
  }
}

resource "aws_sagemaker_endpoint" "this" {
  name                 = var.endpoint_name
  endpoint_config_name = aws_sagemaker_endpoint_configuration.this.name
}

Discussion