{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Amazon SageMaker PyTorch コンテナを用いた MNIST の学習と推論 \n", "\n", "(このノートブックは PyTorch の kernel で実行します)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 目次\n", "\n", "1. [概要](#1.概要)\n", "1. [セットアップ](#2.セットアップ)\n", "1. [データ](#3.データ)\n", "1. [学習](#4.学習)\n", "1. [SageMaker Experiments - 実験管理](#exp)\n", "1. [ハイパーパラメータ調整を用いた学習](#hpo)\n", "1. [SageMaker Autopilot - AutoML の実行](#auto)\n", "\n", "---\n", "\n", "## 1.概要\n", "\n", "MNISTは、手書き文字の分類に広く使用されているデータセットです。 70,000個のラベル付きの28x28ピクセルの手書き数字のグレースケール画像で構成されています。 データセットは、60,000個のトレーニング画像と10,000個のテスト画像に分割されます。 手書きの数字 0から9の合計10のクラスがあります。 このチュートリアルでは、PyTorch を使用して SageMaker で MNIST モデルをトレーニングおよびテストする方法を示します。 \n", "\n", "---\n", "\n", "## 2.セットアップ\n", "\n", "SageMaker セッションを作成し、設定を開始します。\n", "\n", "- モデル学習、デプロイなどに必要なデータを格納する S3 バケットは、ノートブックインスタンス、トレーニングおよびモデルデプロイのためのインスタンスと同じリージョン内にある必要があります。\n", "- モデル学習およびデプロイを行うために、S3にアクセスするなどの必要な権限をもった IAM ロール を指定します。 以下では、ノートブックインスタンスのロールを`sagemaker.get_execution_role()`で取得しており、これを後に利用します。別のロールを指定したい場合は `sagemaker.get_execution_role()` を適切な IAM ロール arn 文字列に置き換えてください。\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import sagemaker\n", "\n", "sagemaker_session = sagemaker.Session()\n", "\n", "bucket = sagemaker_session.default_bucket()\n", "prefix = 'sagemaker/DEMO-pytorch-mnist'\n", "\n", "role = sagemaker.get_execution_role()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import os, json\n", "NOTEBOOK_METADATA_FILE = \"/opt/ml/metadata/resource-metadata.json\"\n", "if os.path.exists(NOTEBOOK_METADATA_FILE):\n", " with open(NOTEBOOK_METADATA_FILE, \"rb\") as f:\n", " metadata = json.loads(f.read())\n", " domain_id = metadata.get(\"DomainId\")\n", " on_studio = True if domain_id is not None else False\n", "print(\"Is this notebook runnning on Studio?: {}\".format(on_studio))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 3.データ\n", "### 3.1.データの取得" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "!aws s3 cp s3://fast-ai-imageclas/mnist_png.tgz . --no-sign-request\n", "if on_studio:\n", " !tar -xzf mnist_png.tgz -C /opt/ml --no-same-owner\n", "else:\n", " !tar -xvzf mnist_png.tgz" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from torchvision import datasets, transforms\n", "from torch.utils.data import DataLoader\n", "import torch\n", "import os\n", "\n", "root_dir_studio = '/opt/ml'\n", "data_dir = os.path.join(root_dir_studio,'data') if on_studio else 'data'\n", "training_dir = os.path.join(root_dir_studio,'mnist_png/training') if on_studio else 'mnist_png/training'\n", "test_dir = os.path.join(root_dir_studio,'mnist_png/testing') if on_studio else 'mnist_png/testing'\n", "\n", "os.makedirs(data_dir, exist_ok=True)\n", "\n", "training_data = datasets.ImageFolder(root=training_dir,\n", " transform=transforms.Compose([\n", " transforms.Grayscale(),\n", " transforms.ToTensor(),\n", " transforms.Normalize((0.1307,), (0.3081,))]))\n", "test_data = datasets.ImageFolder(root=test_dir,\n", " transform=transforms.Compose([\n", " transforms.Grayscale(),\n", " transforms.ToTensor(),\n", " transforms.Normalize((0.1307,), (0.3081,))]))\n", "\n", "training_data_loader = DataLoader(training_data, batch_size=len(training_data))\n", "training_data_loaded = next(iter(training_data_loader))\n", "torch.save(training_data_loaded, os.path.join(data_dir, 'training.pt'))\n", "\n", "test_data_loader = DataLoader(test_data, batch_size=len(test_data))\n", "test_data_loaded = next(iter(test_data_loader))\n", "torch.save(test_data_loaded, os.path.join(data_dir, 'test.pt'))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 3.2.データをS3にアップロードする\n", "データセットを S3 にアップロードするには、 `sagemaker.Session.upload_data` 関数を使用します。 戻り値として入力した S3 のロケーションは、後で学習ジョブを実行するときに使用します。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "inputs = sagemaker_session.upload_data(path=data_dir, bucket=bucket, key_prefix=prefix)\n", "print('input spec (in this case, just an S3 path): {}'.format(inputs))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 4.学習\n", "### 4.1.学習スクリプト\n", "`mnist.py` スクリプトは、SageMaker モデルの学習とホスティングに必要なすべてのコードを提供します(`model_fn` はモデルをロードするための関数です)。\n", "学習スクリプトは、SageMaker の外部で実行するトレーニングスクリプトに非常に似ていますが、次のようなさまざまな環境変数を通じてトレーニング環境に関する有用なプロパティにアクセスできます。\n", "\n", "* `SM_MODEL_DIR`: モデルアーティファクトを書き込むディレクトリへのパスを表す文字列。モデルは、推論用ホスティングのためにS3にアップロードされます。\n", "* `SM_NUM_GPUS`: 現在のコンテナで利用可能な GPU の数。\n", "* `SM_CURRENT_HOST`: コンテナネットワーク上の現在のコンテナの名前。\n", "* `SM_HOSTS`: すべてのホストを含む JSON エンコードリスト。\n", "\n", "`fit()`メソッドの呼び出しで1つの入力チャンネル `training` が使用されたとすると、 `SM_CHANNEL_ [channel_name]`の形式に従って以下が設定されます:\n", "\n", "* `SM_CHANNEL_TRAINING`:`training` チャンネルのデータを含むディレクトリへのパスを表す文字列。\n", "\n", "学習に関する環境変数の詳細については、[SageMaker Containers](https://github.com/aws/sagemaker-containers) をご覧ください。\n", "\n", "典型的なトレーニングスクリプトの書き方は下記の通りです。入力チャンネルからデータをロードし、ハイパーパラメーターで学習の設定、モデルを学習、モデルを `model_dir` に保存し、そこから学習済みモデルのホスティングを行います。 ハイパーパラメーターは引数としてスクリプトに渡され、 `argparse.ArgumentParser` インスタンスとして取得できます。\n", "\n", "下記は、このノートブックで使われるスクリプト ` mnist.py` です。SageMaker PyTorch Container で定義されるデフォルトの実装方法に従い [sagemaker-pytorch-containers](https://github.com/aws/sagemaker-pytorch-containers) 、 `input_fn` 、` predict_fn` 、 `output_fn` および ` transform_fn` の実装を使用します。メインガード (``if __name__=='__main__':``) の中のスクリプトは、学習時にのみ実行され、モデルのホスティング時には実行されません。上記のように、必要な `mnist.py` スクリプトに ` model_fn` を実装しています。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 4.2.Estimatorの作成\n", "学習の条件を設定するため、Estimator クラスの子クラスの PyTorch オブジェクトを作成します。 ここでは、PyTorchスクリプト、IAMロール、および(ジョブごとの)ハードウェア構成を渡す PyTorch Estimator を定義しています。また合わせてローカルの `source_dir` を指定することで、依存するスクリプト群をコンテナにコピーして、学習時に使用することが可能です。\n", "\n", "`mnist.py` で定義されているハイパーパラメータをレンジの形で指定してハイパーパラメータ探索を行う場合は、[ハイパーパラメーター調整ジョブをセットアップする](### ハイパーパラメーター調整ジョブをセットアップする) で紹介しています。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from sagemaker.pytorch import PyTorch\n", "\n", "estimator = PyTorch(entry_point=\"mnist_train.py\",\n", " role=role,\n", " source_dir = \"src\",\n", " framework_version='1.8.1',\n", " py_version='py3',\n", " instance_count=1,\n", " instance_type='local',\n", " hyperparameters={\n", " 'batch-size':128,\n", " 'lr': 0.01,\n", " 'epochs': 1,\n", " 'backend': 'gloo'\n", " })" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 4.3.ローカルモードでの学習の実行\n", "`fit()` メソッドで学習ジョブを実行します。`entry_point` で指定したローカルのスクリプトが、学習用のコンテナ内で実行されます。\n", "\n", "ノートブックインスタンスのCPUで学習する場合はinstance_type = 'local'、GPUで学習する場合はlocal_gpuを指定します。インスタンス数は、ノートブックインスタンスの数、すなわち1になるため、 train_instance_countに指定された値が1より大きい場合も1として扱われますが、traini_instance_count > 1 の分散学習はローカル環境でも擬似的に検証できます。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "estimator.fit({'training': inputs})" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 4.4.ローカルモードでモデルの検証" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from sagemaker.pytorch.model import PyTorchModel\n", "pytorch_model = PyTorchModel(model_data = estimator.model_data,\n", " entry_point='mnist_deploy.py',\n", " source_dir = 'src',\n", " framework_version = '1.8.1',\n", " py_version = 'py3',\n", " role = role)\n", "predictor = pytorch_model.deploy(initial_instance_count=1, instance_type='local')" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Select 10 images and labels from test set\n", "import numpy as np\n", "n_sample = 10\n", "n_test = len(test_data)\n", "sampled_index = np.random.choice(n_test, size=n_sample, replace=False)\n", "sampled_testimg = np.array([test_data[i][0].numpy() for i in sampled_index])\n", "sampled_testlabel = np.array([test_data[i][1] for i in sampled_index])\n", "\n", "# Run inference and pick up the most likely label based on the score\n", "prediction_scores = predictor.predict(sampled_testimg)\n", "labels = np.argmax(prediction_scores,axis = 1)\n", "print(\"Predicted labels: {}\".format(labels))\n", "print(\"Ground Truth: {}\".format(sampled_testlabel))" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "predictor.delete_endpoint()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 5. SageMaker Experiments - 実験管理 \n", "\n", "### 5.1 実験管理の全体像\n", "\n", "実験管理は以下のコンポーネントで構成されます。\n", "- Experiment: 複数の Trial を束ねたものです。\n", "- Trial: 1回の実験を表し、実験設定(入力データ、パラメータなど)および実験結果(出力モデル、精度など)の1セットです。\n", "- Trial Component: 1回の実験を任意のステップで分割して、PreprocessingやTrainingなどの名前をつけて記録できます。\n", "- Tracker: Component の中で更に細かく情報を収集するもので、パラメータ、入出力データなどをトラックすることができます。\n", "\n", "\"Drawing\"\n", "\n", "\n", "以下では1つのExperimentの中で、2つの異なる学習率で Trial を行います。Trial Component は前処理と学習の2つに分割します。前処理に関しては、同じ入力データに対して、同じ PyTorch による標準化を行うので、異なる Trial でも同じ前処理 (同じ Trial Component)を使うことになります。学習に関しては、学習率が異なるので、異なる処理(異なる Trial Component) になります。\n", "\n", "実験で作成されるテーブルは概ね以下のようになります。列名や数値は一例です。各行は Trial Component に対応し、それぞれ対応するComponent Name が表示されます。ここでは2種類の学習 (t1, t2) と、それぞれの前処理 (t3) が記録されています。それぞれの Trial Component が紐づく Trial も表示されます。t1とt2は、それぞれ Trial X と Y に紐づき、共通の t3 は X と Y の両方に紐づきます。Trial Component は全ての列に対して値をもっているわけではありません。例えば、学習の Trial Component に入力データに関する値を保持させず、前処理の Trial Component に保存する場合、以下のように入力データの列に NaN が表示されます。同様に、前処理の際に精度はわからないため NaN と表示されます。\n", "\n", "| TrialComponent | ComponentName | Trial | 学習率 | 精度 (最大値) | インスタンスタイプ | 入力データの標準化 | 入力データのS3パス|\n", "| --- | --- | --- |--- | --- | --- | --- | --- | \n", "| t1 | Training | X | 0.01 | 0.83 | ml.m4.xlarge | NaN | NaN | \n", "| t2 | Training | Y | 0.02 | 0.85 | ml.m4.xlarge | NaN | NaN | \n", "| t3 | Preprocessing | [X,Y] | NaN | NaN | NaN | 0.1307 | s3://data-input/examples.pt |\n", "\n", "\n", "それでは実験管理を行ってみましょう。まず sagemaker-experiments のライブラリをインストールして、必要なライブラリをインポートします。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import sys\n", "!{sys.executable} -m pip install sagemaker-experiments\n", "\n", "from sagemaker.analytics import ExperimentAnalytics\n", "from smexperiments.experiment import Experiment\n", "from smexperiments.trial import Trial\n", "from smexperiments.trial_component import TrialComponent\n", "from smexperiments.tracker import Tracker" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 5.2 実験の作成\n", "\n", "今回は1つの実験を作成します。実験は実験の名前と説明文をパラメータとしてもちます。上記の図で示したように、この実験に Trial Component がぶらさがります。Trial Component をぶらさげるときは実験名で指定しますので、実験名は唯一である必要があります。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import time\n", "import boto3\n", "\n", "sm_client = boto3.Session().client('sagemaker')\n", "\n", "mnist_experiment = Experiment.create(\n", " experiment_name=f\"mnist-hand-written-digits-classification-{int(time.time())}\", \n", " description=\"Classification of mnist hand-written digits\", \n", " sagemaker_boto_client=sm_client)\n", "print(\"Experiment \\\"{}\\\" is created!\".format(mnist_experiment.experiment_name))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 5.3 前処理の Trial Component の作成\n", "\n", "この Trial Component は必須ではありませんが、通常、どういった前処理を行ったか、どういったデータを使ったかは、再現性の観点から記録しておくことが望ましいです。記録する際は、Tracker というのを作成し、log_parameters や log_input を使用して情報を入力します。Tracker で収集した情報を Trial Component にしておき、後に Trial に紐付けます。 " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "with Tracker.create(display_name=\"Preprocessing\", sagemaker_boto_client=sm_client) as tracker:\n", " tracker.log_parameters({\n", " \"normalization_mean\": 0.1307,\n", " \"normalization_std\": 0.3081,\n", " })\n", " # we can log the s3 uri to the dataset we just uploaded\n", " tracker.log_input(name=\"mnist-dataset\", media_type=\"s3/uri\", value=inputs)\n", "\n", "preprocessing_trial_component = tracker.trial_component" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 5.4 Trial の作成と Trial Component の紐付け\n", "\n", "今回は学習率を 0.01 と 0.02 に変えて実験を行います。Trial はそれぞれの値で実行するので計2回です。変更点は学習率のみで、その他の Trial 作成方法は同じなので、for 文で作成します。for 文内の流れは以下のとおりです。\n", "\n", "1. Trial を作成する\n", "2. Trial に前処理の Trial Component を紐付ける\n", "3. Trial に学習の Trial Component を紐付ける \n", " 3.1 通常の学習と同様に Estimator を作成する \n", " 3.2 Estimator に対して fit する際に Trial に紐付ける \n", " \n", "**Estimator の情報は自動的に記録されるため、Trial Component や Tracker を明示的に用意する必要はありません**\n", "\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from sagemaker.pytorch import PyTorch\n", "\n", "lr_candidates = [0.01, 0.02]\n", "for i, lr in enumerate(lr_candidates):\n", " # create trial\n", " trial_name = f\"cnn-training-job-{i}-learning-rate-{int(time.time())}\"\n", " cnn_trial = Trial.create(\n", " trial_name=trial_name, \n", " experiment_name=mnist_experiment.experiment_name,\n", " sagemaker_boto_client=sm_client,\n", " )\n", " \n", " # associate the proprocessing trial component with the current trial\n", " cnn_trial.add_trial_component(preprocessing_trial_component)\n", " \n", " # all input configurations, parameters, and metrics specified in estimator \n", " # definition are automatically tracked\n", " estimator = PyTorch(\n", " py_version='py3',\n", " entry_point='mnist_train.py',\n", " source_dir='src',\n", " role=role,\n", " framework_version='1.8.1',\n", " instance_count=1,\n", " instance_type='ml.c4.xlarge',\n", " hyperparameters={\n", " 'epochs': 2,\n", " 'backend': 'gloo',\n", " 'lr': lr\n", " },\n", " metric_definitions=[\n", " {'Name':'test:loss', 'Regex':'Average loss: (.*?),'},\n", " {'Name':'test:accuracy', 'Regex':'Accuracy: [0-9]+/[0-9]+ \\((.*?)%\\)'}\n", " ],\n", " enable_sagemaker_metrics=True\n", " )\n", " \n", " cnn_training_job_name = \"cnn-training-job-{}\".format(int(time.time()))\n", " \n", " # Now associate the estimator with the Experiment and Trial\n", " estimator.fit(\n", " inputs={'training': inputs}, \n", " job_name=cnn_training_job_name,\n", " experiment_config={\n", " \"ExperimentName\": mnist_experiment.experiment_name,\n", " \"TrialName\": cnn_trial.trial_name,\n", " \"TrialComponentDisplayName\": \"Training\",\n", " },\n", " wait=False,\n", " )\n", " \n", " # give it a while before dispatching the next training job\n", " time.sleep(2)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "cnn_trial.trial_name" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 5.5 実験結果の確認\n", "\n", "#### 5.5.1 全体の確認\n", "\n", "上記では学習ジョブをバックグラウンドで実行しており、すぐにこのセルに移動してきた場合、まだ学習は終わっていない可能性があります。そのような状況でも、すでに記録済みの情報、例えば、設定した学習率や入力データの情報をみることができます。\n", "\n", "実験結果は情報が追加されるたびに更新されます。学習の実行が完了していれば、各ジョブの精度も確認できるようになります。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import pandas as pd\n", "# show 30 columns at most\n", "pd.set_option('display.max_columns', 50)\n", "\n", "trial_component_analytics = ExperimentAnalytics(\n", " sagemaker_session=sagemaker_session, \n", " experiment_name=mnist_experiment.experiment_name)\n", "df_trial = trial_component_analytics.dataframe()\n", "df_trial" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### 5.5.2 実験結果のフィルタリング\n", "\n", "例えば、Trial Component の Display Name が Training、つまり学習の Trial Component のみ知りたい場合は以下のようなフィルタを作成して表示します。それ以外にも、精度などの metrics やパラメータなど、表示列を絞ったり、精度の高い順にソートしたりすることができます。 " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "search_expression = {\n", " \"Filters\":[\n", " {\n", " \"Name\": \"DisplayName\",\n", " \"Operator\": \"Equals\",\n", " \"Value\": \"Training\",\n", " }\n", " ],\n", "}\n", "trial_component_analytics = ExperimentAnalytics(\n", " sagemaker_session=sagemaker_session, \n", " experiment_name=mnist_experiment.experiment_name,\n", " search_expression=search_expression,\n", " sort_by=\"metrics.test:accuracy.max\",\n", " sort_order=\"Descending\",\n", " metric_names=['test:accuracy','test:loss'],\n", " parameter_names=['lr', 'epochs', 'dropout', 'optimizer']\n", ")\n", "df_trial = trial_component_analytics.dataframe()\n", "df_trial" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### 5.5.4 Lineage の確認\n", "\n", "5.5.3 で学習の Trial の一覧を見ましたが、そのうちの1つが気になった場合、その学習に関する情報だけではなく、前処理の情報もみたくなると思います。学習も前処理も1つの Trial に結びつけていますので、Trial で検索すれば、双方を確認することができます。\n", "\n", "上の一覧の1行目の Trial (`df_trial.Trials[0][0]`) を見たい場合は以下のようにします。Training と Preprocessing が表示されているでしょうか。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "focus_trial = df_trial.Trials[0][0]\n", "\n", "lineage_table = ExperimentAnalytics(\n", " sagemaker_session=sagemaker_session, \n", " search_expression={\n", " \"Filters\":[{\n", " \"Name\": \"Parents.TrialName\",\n", " \"Operator\": \"Equals\",\n", " \"Value\": focus_trial\n", " }]\n", " },\n", " sort_by=\"CreationTime\",\n", " sort_order=\"Ascending\",\n", ")\n", "\n", "lineage_table.dataframe()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 5.6 実験結果にもとづいてモデルをデプロイ\n", "\n", "さきほど、実験結果に対してテストデータに対する精度の降順で、 Training の Trial Component をソートし、 `df_trial` に保存しました。つまり、`df_trial`の1行目のデータは精度が最も高い Training の情報を記録しています。最も精度の良いモデルをデプロイするため、df_trial の1行目の情報を `load` して、保存されているモデルのS3パスを `SageMaker.ModelArtifact` から取得しましょう。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#Pulling best based on sort in the analytics/dataframe so first is best....\n", "\n", "trial_component_analytics = ExperimentAnalytics(\n", " sagemaker_session=sagemaker_session, \n", " experiment_name=mnist_experiment.experiment_name)\n", "df_trial = trial_component_analytics.dataframe()\n", "\n", "best_trial_component_name = df_trial.iloc[0]['TrialComponentName']\n", "best_trial_component = TrialComponent.load(best_trial_component_name)\n", "model_data = best_trial_component.output_artifacts['SageMaker.ModelArtifact'].value\n", "model_data" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "モデルの S3 パスが分かればデプロイすることが可能です。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from sagemaker.pytorch import PyTorchModel\n", "pytorch_model = PyTorchModel(model_data = model_data,\n", " entry_point='mnist_deploy.py',\n", " source_dir = 'src',\n", " framework_version = '1.8.1',\n", " py_version = 'py3',\n", " role = role,\n", " name=best_trial_component.trial_component_name)\n", "predictor = pytorch_model.deploy(initial_instance_count=1, instance_type='ml.m5.xlarge')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "推論を実行してみます。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Select 10 images and labels from test set\n", "import numpy as np\n", "n_sample = 10\n", "n_test = len(test_data)\n", "sampled_index = np.random.choice(n_test, size=n_sample, replace=False)\n", "sampled_testimg = np.array([test_data[i][0].numpy() for i in sampled_index])\n", "sampled_testlabel = np.array([test_data[i][1] for i in sampled_index])\n", "\n", "# Run inference and pick up the most likely label based on the score\n", "prediction_scores = predictor.predict(sampled_testimg)\n", "labels = np.argmax(prediction_scores,axis = 1)\n", "print(\"Predicted labels: {}\".format(labels))\n", "print(\"Ground Truth: {}\".format(sampled_testlabel))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "不要になったエンドポイントは削除しておきましょう。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "predictor.delete_endpoint()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 6 ハイパーパラメータ調整を用いた学習\n", "### 6.1 ハイパーパラメーター調整ジョブの Estimator を設定\n", "*以下のデフォルト設定では、ハイパーパラメーター調整ジョブが完了するまでに10分程度かかります。*\n", "\n", "この例では、 SageMaker Python SDK を使用して、ハイパーパラメーターの最適化を取り入れた学習を行います。先ほどはノートブックインスタンス上で学習を行うのローカルモードを扱ったり、様々な設定を Experiments に記録しながら実行したりしました。最終的には実験結果を確認して、最も良いモデルをデプロイしました。ハイパーパラメータ調整は、実験管理で行ったようにデータやパラメータをユーザが決めて実行するのではなく、特定のデータに対して最適なパラメータを探索します。パラメータを決めるのがユーザか、SageMaker による自動化か、という点が異なります。ハイパーパラメータ調整は、とにかく手軽に最高の精度のモデルを得る目的においては便利です。完全に自動化するため、ユーザのパラメータ調整の方針を取り入れにくい点があります。\n", "\n", "SageMaker は、学習率、バッチサイズ、エポック数などのパラメータの最適値を探索するためのインターフェースを持っています。探索したいハイパーパラメーターごとに、連続値として探索する場合はその範囲、またはリストの中から探索したい場合は探索可能な値のリストを指定します。ハイパーパラメーター調整ジョブは、異なるハイパーパラメーター設定で複数のトレーニングジョブを並列に起動し、定義済みの `objective metric` (目的関数) に基づいてそれらのトレーニングジョブの結果を評価します。以前の結果に基づいて次のハイパーパラメーター探索の設定を選択します。ハイパーパラメーター調整ジョブごとに、一度に並列で実行する学習インスタンス数、最大の学習ジョブ数、をそれぞれ割り当て、最大の学習ジョブ数が実行されると探索を終え、`objective metric` に対して最適なパフォーマンスを達成したハイパーパラメータの組み合わせを返します。\n", "\n", "次に、以下の手順に従って、SageMaker Python SDK を使用してハイパーパラメーター調整ジョブをセットアップします。\n", "* `estimator` を作成して PyTorch 学習ジョブをセットアップします。\n", "* 調整するハイパーパラメーターの範囲を定義します。この例では、学習率とバッチサイズのハイパーパラメータ探索範囲を設定します。\n", "* 最適化するチューニングジョブの `objective metric` を定義します。\n", "* 上記の設定で `HyperparameterTuner` オブジェクトを設定します。\n", "\n", "Spot Instanceを用いて実行する場合は、下記のコードを Estimator の train_instance_type の次の行に追加しましょう。\n", "\n", "```python\n", " max_run = 5000,\n", " use_spot_instances = 'True',\n", " max_wait = 10000,\n", "```" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from sagemaker.pytorch import PyTorch\n", "hpo_estimator = PyTorch(entry_point=\"mnist_train.py\",\n", " role=role,\n", " source_dir ='src',\n", " framework_version='1.8.1',\n", " py_version='py3',\n", " instance_count=1,\n", " instance_type='ml.m4.xlarge',\n", " hyperparameters={\n", " 'epochs': 4,\n", " 'backend': 'gloo'\n", " })" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "`Estimator` を定義したら、調整するハイパーパラメーターとその探索範囲を指定します。ハイパーパラメーターの探索範囲の指定は3種類の方法があります。\n", "- カテゴリーパラメーターは、探索したい値のリストを `CategoricalParameter(list)` で定義します。このリストの中から最適な値を探索します。 \n", "- 連続パラメーターは、`ContinuousParameter(min、max)` で定義される最小値と最大値の間の連続空間で、任意の実数値から探索を行います。\n", "- 整数パラメーターは、`IntegerParameter(min、max)` で定義された最小値と最大値の間の任意の整数値で、探索を行います。\n", "\n", "* 可能であれば、値を最も制限の少ないタイプとして指定することをお勧めします。 たとえば、0.01〜0.2の連続値として学習率を調整すると、0.01、0.1、0.15、または0.2の値を持つカテゴリパラメーターとして調整するよりも良い結果が得られる可能性があります。 バッチサイズは一般的に2のべき乗であることが推奨されているため、ここではカテゴリパラメータとしてバッチサイズを指定しています。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from sagemaker.tuner import ContinuousParameter, CategoricalParameter\n", "hyperparameter_ranges = {'lr': ContinuousParameter(0.001, 0.1),\n", " 'batch-size': CategoricalParameter([32,64,128,256])}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "次に objective metric とその定義を指定します。これには、トレーニングジョブの CloudWatch ログからそのメトリックを抽出するために必要な正規表現(Regex)が含まれます。 この特定のケースでは、スクリプトは平均損失値を出力し、それを objective metric として使用します。また、`objective_type` を `minimize` に設定します。これにより、ハイパーパラメーターチューニングは、最適なハイパーパラメーター設定を検索するときに客観的なメトリックを最小化しようとします。 デフォルトでは、 `objective_type` は `maximize` に設定されています。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "objective_metric_name = 'average test loss'\n", "objective_type = 'Minimize'\n", "metric_definitions = [{'Name': 'average test loss',\n", " 'Regex': 'Average loss: (.*?),'}]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 6.2 ハイパーパラメーター調整ジョブの tuner 設定する\n", "次に、 `HyperparameterTuner` オブジェクトを作成します。\n", "- 上記で作成した PyTorch 推定器\n", "- ハイパーパラメーターの範囲\n", "- Objective metric 名と定義\n", "- 合計で実行するトレーニングジョブの数や並行して実行できるトレーニングジョブの数などのリソース構成の調整。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from sagemaker.tuner import HyperparameterTuner\n", "tuner = HyperparameterTuner(hpo_estimator,\n", " objective_metric_name,\n", " hyperparameter_ranges,\n", " metric_definitions,\n", " max_jobs=2,\n", " max_parallel_jobs=2,\n", " objective_type=objective_type)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 6.3 ハイパーパラメーター調整ジョブを起動する\n", "`.fit()`を呼び出し、学習およびテストデータセットへの S3 パスを渡すことで、ハイパーパラメーターチューニングジョブを開始できます。\n", "\n", "ハイパーパラメーターチューニングジョブが作成されたら、次のステップでチューニングジョブを記述してその進行状況を確認できます。SageMaker コンソールからジョブに移動して、ハイパーパラメーターチューニングジョブの進行状況を確認できます。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "tuner.fit({'training': inputs})\n", "tuner.wait()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 6.4 ハイパーパラメータ調整の結果を確認する\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "tuner.analytics().dataframe()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 6.5 最良モデルをデプロイしてエンドポイントを作成する\n", "トレーニング完了後、Tuner オブジェクトを使用して、`PyTorchPredictor` をビルドおよびデプロイします。前の手順では、Tuner が複数のトレーニングジョブを起動し、最適な obejctive metric を持つ結果のモデルが最適なモデルとして定義しました。これにより、SageMaker エンドポイントが作成されます。これにより、チューナーが探索した最適なモデルに基づいて推論を実行するためのホスティングを行います。\n", "\n", "tuner.best_estimator() によって、最善の Estimator の情報を取り出すことができ、そこから model_data で最良のモデルへのパスを調べます。\n", "それ以降はこれまでの deploy と同じです。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from sagemaker.pytorch import PyTorchModel\n", "model_data = tuner.best_estimator().model_data\n", "pytorch_model = PyTorchModel(model_data = model_data,\n", " entry_point='mnist_deploy.py',\n", " source_dir = 'src',\n", " framework_version = '1.8.1',\n", " py_version = 'py3',\n", " role = role,\n", " name=best_trial_component.trial_component_name)\n", "predictor = pytorch_model.deploy(initial_instance_count=1, instance_type='ml.m5.xlarge')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 6.6 評価" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Select 10 images and labels from test set\n", "import numpy as np\n", "n_sample = 10\n", "n_test = len(test_data)\n", "sampled_index = np.random.choice(n_test, size=n_sample, replace=False)\n", "sampled_testimg = np.array([test_data[i][0].numpy() for i in sampled_index])\n", "sampled_testlabel = np.array([test_data[i][1] for i in sampled_index])\n", "\n", "# Run inference and pick up the most likely label based on the score\n", "prediction_scores = predictor.predict(sampled_testimg)\n", "labels = np.argmax(prediction_scores,axis = 1)\n", "print(\"Predicted labels: {}\".format(labels))\n", "print(\"Ground Truth: {}\".format(sampled_testlabel))" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "predictor.delete_endpoint()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 7. SageMaker Autopilot - AutoML の実行\n", "\n", "(注意:AutoMLは30分程度かかります)\n", "\n", "### 7.1 CSV ファイルの用意\n", "\n", "Autopilot は 表データ (CSV ファイル) に対して実行することができます。そこでまず mnist のデータである 28x28 の画像を 784 個の1行のデータにして、データ数 60000 x 画像 784 の表データに変換します。加えて、785列目にラベルの情報を加えます。\n", "\n", "ヘッダを $x_0,x_1, \\cdots, x_{783}, y$ として与えて、csv 形式で保存します。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from torch.utils.data import DataLoader\n", "import pandas as pd\n", "\n", "# Create 60000x 785 matrix. 785 = 784 features + 1 label. \n", "all_train_loader = DataLoader(training_data, batch_size=len(training_data))\n", "X_train = next(iter(all_train_loader))[0].numpy()\n", "y_train = next(iter(all_train_loader))[1].numpy()\n", "X_train = X_train.reshape([60000, 28*28])\n", "y_train = y_train.reshape([60000,1])\n", "train_csv = np.concatenate([X_train,y_train], axis = 1)\n", "\n", "# Headers are x0, x1, ... x,783, y\n", "headers = [\"x\" + str(i) for i in range(28*28)] + [\"y\"]\n", "\n", "# Create dataframe with headers to write csv\n", "train_df = pd.DataFrame(train_csv, columns = headers)\n", "train_df['y'] = train_df['y'].astype(int)\n", "train_df.to_csv(\"mnist.csv\", index=False)\n", "train_df.head()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 7.2 ファイルのアップロード" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "input_csv = sagemaker_session.upload_data(path='mnist.csv', bucket=bucket, key_prefix=prefix)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 7.3 AutoML の実行\n", "\n", "CSV の中でラベルの列名 `y` や、最大実行回数 `max_candidates` を指定して、先程アップロードした csv に対して fit すれば AutoML を実行可能です。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from sagemaker import AutoML\n", "from time import gmtime, strftime, sleep\n", "\n", "timestamp_suffix = strftime('%d-%H-%M-%S', gmtime())\n", "base_job_name = 'automl-mnist-sdk-' + timestamp_suffix\n", "\n", "target_attribute_name = 'y'\n", "\n", "automl = AutoML(role=role,\n", " target_attribute_name=target_attribute_name,\n", " base_job_name=base_job_name,\n", " sagemaker_session=sagemaker_session,\n", " max_candidates=2)\n", "automl.fit(input_csv, job_name=base_job_name, wait=False, logs=False)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 7.4 実行状況の確認\n", "\n", "describe_auto_ml_job()を利用すると、AutoML の実行状況を見ることが可能です。以下では、AutoML の実行状況を InProgress のあいだ、監視し続けます。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print ('JobStatus - Secondary Status')\n", "print('------------------------------')\n", "\n", "\n", "describe_response = automl.describe_auto_ml_job()\n", "print (describe_response['AutoMLJobStatus'] + \" - \" + describe_response['AutoMLJobSecondaryStatus'])\n", "job_run_status = describe_response['AutoMLJobStatus']\n", " \n", "while job_run_status not in ('Failed', 'Completed', 'Stopped'):\n", " describe_response = automl.describe_auto_ml_job()\n", " job_run_status = describe_response['AutoMLJobStatus']\n", " \n", " print(describe_response['AutoMLJobStatus'] + \" - \" + describe_response['AutoMLJobSecondaryStatus'])\n", " sleep(30)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 7.5 最良の学習モデルの確認\n", "\n", "describe_auto_ml_job() の `BestCandidate` から最良のモデルに関する情報を取得することができます。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "best_candidate = automl.describe_auto_ml_job()['BestCandidate']\n", "best_candidate_name = best_candidate['CandidateName']\n", "print(best_candidate)\n", "print('\\n')\n", "print(\"CandidateName: \" + best_candidate_name)\n", "print(\"FinalAutoMLJobObjectiveMetricName: \" + best_candidate['FinalAutoMLJobObjectiveMetric']['MetricName'])\n", "print(\"FinalAutoMLJobObjectiveMetricValue: \" + str(best_candidate['FinalAutoMLJobObjectiveMetric']['Value']))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 7.6 最良のモデルでバッチ推論を行う\n", "\n", "テストデータの CSV を用意して、それらに対するラベルを最良のモデルで一括して推論しましょう。まずはテストデータの CSV を用意します。テストデータは header が不要です。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Create 10000 x 784 matrix. \n", "all_test_loader = DataLoader(test_data, batch_size=len(test_data))\n", "X_test = next(iter(all_test_loader))[0].numpy()\n", "X_test = X_test.reshape([10000, 28*28])\n", "np.savetxt(\"mnist_test.csv\", X_test, delimiter=\",\")\n", "input_test_csv = sagemaker_session.upload_data(path='mnist_test.csv', bucket=bucket, key_prefix=prefix)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "best_candudidate から最良のモデルデータを取り出し、model に保存します。バッチ推論を行うためには、model に対して transformer を作成して、transform を実行します。1行ごとにデータが記載されているので、split_type を Line に、 content_type は text/csv にします。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "s3_transform_output_path = 's3://{}/{}/inference-results/'.format(bucket, prefix);\n", "inference_response_keys = ['predicted_label', 'probability']\n", "\n", "model = automl.create_model(name=best_candidate['CandidateName'],\n", " candidate=best_candidate,\n", " inference_response_keys=inference_response_keys)\n", " \n", "output_path = s3_transform_output_path + best_candidate['CandidateName'] +'/'\n", " \n", "transformer = model.transformer(instance_count=1, \n", " instance_type='ml.m5.xlarge',\n", " assemble_with='Line',\n", " output_path=output_path)\n", "\n", "transformer.transform(data=input_test_csv, split_type='Line', content_type='text/csv', wait=False)\n", "print(\"Starting transform job {}\".format(transformer._current_job_name))\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 7.7 評価\n", "\n", "しばらくするとバッチ推論を行った結果が S3 に保存されますので、それを get_csv_from_s3 でダウンロードします。0列目は推定ラベル、1列目はその確からしさです。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import json\n", "import io\n", "from urllib.parse import urlparse\n", "\n", "def get_csv_from_s3(s3uri, file_name):\n", " parsed_url = urlparse(s3uri)\n", " bucket_name = parsed_url.netloc\n", " prefix = parsed_url.path[1:].strip('/')\n", " s3 = boto3.resource('s3')\n", " obj = s3.Object(bucket_name, '{}/{}'.format(prefix, file_name))\n", " return obj.get()[\"Body\"].read().decode('utf-8')\n", "\n", "pred_csv = get_csv_from_s3(transformer.output_path, '{}.out'.format(\"mnist_test.csv\"))\n", "prediction = pd.read_csv(io.StringIO(pred_csv), header=None)\n", "prediction" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "推論結果と正解を比較しましょう。まず正解データを取り出します。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "y_test = next(iter(all_test_loader))[1].numpy()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "scikit-learnを使って評価します。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from sklearn.metrics import accuracy_score\n", "accuracy_score(y_test, prediction.values[:,0])" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "best_candidate['CandidateName']" ] } ], "metadata": { "instance_type": "ml.t3.medium", "kernelspec": { "display_name": "Python 3 (PyTorch 1.6 Python 3.6 CPU Optimized)", "language": "python", "name": "python3__SAGEMAKER_INTERNAL__arn:aws:sagemaker:us-east-1:081325390199:image/pytorch-1.6-cpu-py36-ubuntu16.04-v1" }, "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.6.13" }, "notice": "Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. Licensed under the Apache License, Version 2.0 (the \"License\"). You may not use this file except in compliance with the License. A copy of the License is located at http://aws.amazon.com/apache2.0/ or in the \"license\" file accompanying this file. This file is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License." }, "nbformat": 4, "nbformat_minor": 4 }