{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Amazon SageMaker PyTorch コンテナを用いた MNIST の学習と推論 " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 目次\n", "\n", "1. [背景](#1.背景)\n", "1. [セットアップ](#2.セットアップ)\n", "1. [データ](#3.データ)\n", "1. [学習](#4.学習)\n", "1. [ハイパーパラメータ調整を用いた学習](#5.ハイパーパラメータ調整を用いた学習)\n", "1. [ホスティング](#6.ホスティング)\n", "1. [リソースの削除](#7.リソースの削除)\n", "\n", "---\n", "\n", "## 1.背景\n", "\n", "MNISTは、手書き文字の分類に広く使用されているデータセットです。 70,000個のラベル付きの28x28ピクセルの手書き数字のグレースケール画像で構成されています。 データセットは、60,000個のトレーニング画像と10,000個のテスト画像に分割されます。 手書きの数字 0から9の合計10のクラスがあります。 このチュートリアルでは、PyTorch を使用して SageMaker で MNIST モデルをトレーニングおよびテストする方法を示します。 また、SageMaker の自動モデルチューニングを使用して適切なハイパーパラメーターを選択し、最適なモデルを取得する方法をご説明します。\n", "\n", "SageMaker の PyTorch の詳細については、[sagemaker-pytorch-containers](https://github.com/aws/sagemaker-pytorch-containers) と [sagemaker-python-sdk](https://github.com/aws/sagemaker-python-sdk) のレポジトリをご参照ください。\n", "\n", "---\n", "\n", "## 2.セットアップ\n", "\n", "SageMaker セッションを作成し、設定を開始します。\n", "\n", "- 学習およびモデルデータに使用する S3 バケットとプレフィックスは、ノートブックインスタンス、トレーニング、およびホスティングと同じリージョン内にある必要があります。\n", "- データへの学習およびホスティングアクセスを提供するために使用される IAM ロール arn を用います。 ノートブックインスタンス、学習インスタンス、および/またはホスティングインスタンスに複数のロールが必要な場合は、 `sagemaker.get_execution_role()` を、適切な IAM ロール arn 文字列に置き換えてください。\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import sagemaker\n", "from sagemaker.tuner import IntegerParameter, CategoricalParameter, ContinuousParameter, HyperparameterTuner\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": "markdown", "metadata": {}, "source": [ "このノートブックのコードは、以前からのノートブックインスタンスで実行する場合と、SageMaker Studio のノートブックで実行する場合とで挙動が異なります。以下のセルを実行することで、いまの実行環境が以前からのノートブックインスタンスなのか、SageMaker Studio のノートブックなのかを判定して、`on_studio`に記録します。この結果に基づいて、以降のノートブックの実行を次のように変更します。\n", "\n", "- データセットの展開先を変更します。SageMaker Studio を利用する場合、home のディレクトリは EFS をマウントして実現されており、データセットを展開する際にやや時間を要します。そこで home 以外のところへ展開するようにします。\n", "- SageMaker Studio では学習・推論の local モードがサポートされていません。従って、Studio では local モードを行わないようにします。" ] }, { "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": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "!pygmentize mnist.py" ] }, { "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", "if on_studio:\n", " instance_type = 'ml.m4.xlarge'\n", "else:\n", " instance_type = 'local'\n", "\n", "estimator = PyTorch(entry_point=\"mnist.py\",\n", " role=role,\n", " framework_version='1.8.1',\n", " py_version='py3',\n", " instance_count=1,\n", " instance_type=instance_type,\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", "\n", "***以前のノートブックインスタンスのみローカルモードを利用できます。Studio の場合は ml.m4.xlarge を利用します。***\n", "\n", "\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.ローカルモードでモデルの検証\n", "\n", "***以前のノートブックインスタンスのみローカルモードを利用できます。Studio の場合は ml.m4.xlarge を利用します。***" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "predictor = estimator.deploy(initial_instance_count=1, instance_type=instance_type)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 4.5.画像データに対する予測の実行\n", "\n", "正規化していない MNIST データを正解データとして確認するために読み込んでおきます。テストデータから5枚をランダムにサンプリングして予測してみます。予測のために`predictor`に画像を入力します。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%matplotlib inline\n", "import random\n", "import numpy as np\n", "import matplotlib.pyplot as plt\n", "\n", "raw_test_data = datasets.ImageFolder(root=test_dir,\n", " transform=transforms.Compose([\n", " transforms.Grayscale(),\n", " transforms.ToTensor()]))\n", "num_samples = 5\n", "indices = random.sample(range(len(raw_test_data) - 1), num_samples)\n", "raw_images = np.array([raw_test_data[i][0].numpy() for i in indices])\n", "raw_labels = np.array([raw_test_data[i][1] for i in indices])\n", "images = np.array([test_data[i][0].numpy() for i in indices])\n", "\n", "for i in range(num_samples):\n", " plt.subplot(1,num_samples,i+1)\n", " plt.imshow(raw_images[i].reshape(28, 28), cmap='gray')\n", " plt.title(raw_labels[i])\n", " plt.axis('off')\n", "\n", "prediction = predictor.predict(images)\n", "predicted_label = prediction.argmax(axis=1)\n", "print('The predicted labels are: {}'.format(predicted_label))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 5 ハイパーパラメータ調整を用いた学習\n", "### 5.1.ハイパーパラメーター調整ジョブの Estimator を設定\n", "*以下のデフォルト設定では、ハイパーパラメーター調整ジョブが完了するまでに10分程度かかります。*\n", "\n", "この例では、 SageMaker Python SDK を使用して、ハイパーパラメーターの最適化を取り入れた学習を行います。先ほどはノートブックインスタンス上で学習を行うのローカルモードを扱いましたが、ここでは、`ml.m4.xlarge` を学習インスタンスとして個別に起動し、その上にPyTorchコンテナを起動し、学習する方法を説明します。\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", "\n", "hpo_estimator = PyTorch(entry_point=\"mnist.py\",\n", " role=role,\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": [ "hyperparameter_ranges = {'lr': ContinuousParameter(0.001, 0.1),'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': 'Test set: Average loss: ([0-9\\\\.]+)'}]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 5.2.ハイパーパラメーター調整ジョブの tuner 設定する\n", "次に、 `HyperparameterTuner` オブジェクトを作成します。\n", "- 上記で作成した PyTorch 推定器\n", "- ハイパーパラメーターの範囲\n", "- Objective metric 名と定義\n", "- 合計で実行するトレーニングジョブの数や並行して実行できるトレーニングジョブの数などのリソース構成の調整。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "tuner = HyperparameterTuner(hpo_estimator,\n", " objective_metric_name,\n", " hyperparameter_ranges,\n", " metric_definitions,\n", " max_jobs=4,\n", " max_parallel_jobs=2,\n", " objective_type=objective_type)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 5.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.ホスティング\n", "### 6.1.エンドポイントを作成する\n", "トレーニング完了後、Tuner オブジェクトを使用して、`PyTorchPredictor` をビルドおよびデプロイします。前の手順では、Tuner が複数のトレーニングジョブを起動し、最適な obejctive metric を持つ結果のモデルが最適なモデルとして定義ました。これにより、SageMaker エンドポイントが作成されます。これにより、チューナーが探索した最適なモデルに基づいて推論を実行するためのホスティングを行います。\n", "\n", "deploy 関数の引数により、エンドポイントに使用されるインスタンスの数とタイプを設定できます。これらは、トレーニングジョブで使用した値と同じである必要はありません。ここでは、モデルを単一の ```ml.m4.xlarge``` インスタンスにデプロイします。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "predictor_hpo = tuner.deploy(initial_instance_count=1, instance_type='ml.m4.xlarge')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 6.2.評価\n", "Estimator を使用して、手書きの数字を分類できるようになりました。Jupyer Notebook のみで利用可能です。 (Jupyter lab では利用できません)\n", "\n", "下のセルを実行すると、空の画像ボックスが表示されます。 次に、その中に数字を描画すると、ピクセルデータがこのノートブックの `data` 変数にロードされ、`predictor` に渡すことができます。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from IPython.display import HTML\n", "HTML(open(\"input.html\").read())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### - HPOを用いた学習の結果" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "image = np.array([data], dtype=np.float32)\n", "\n", "response_hpo = predictor_hpo.predict(image)\n", "prediction_hpo = response_hpo.argmax(axis=1)[0]\n", "print(prediction_hpo)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### - ローカルモードの学習の結果" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "image = np.array([data], dtype=np.float32)\n", "\n", "response = predictor.predict(image)\n", "prediction = response.argmax(axis=1)[0]\n", "print(prediction)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 7.リソースの削除\n", "上記で作成したホスティングエンドポイントは、明示的に削除しないと立ち上がったままになり、課金が継続されます。不要な課金を防ぐために、このノートブックの実行が終了したらエンドポイントを削除しましょう。立ち上がっている同一リージョン内のエンドポイントは、SageMaker マネージメントコンソールの「推論」->「エンドポイント」から一覧を確認できます。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# tuner で作ったエンドポイントの削除\n", "predictor_hpo.delete_endpoint()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "instance_type": "ml.t3.medium", "kernelspec": { "display_name": "Python 3 (PyTorch 1.4 Python 3.6 CPU Optimized)", "language": "python", "name": "python3__SAGEMAKER_INTERNAL__arn:aws:sagemaker:us-west-2:236514542706:image/pytorch-1.4-cpu-py36" }, "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 }