自前の機械学習プログラムをSageMaker Python SDKを用いてAmazon SageMaker上で動かす

こんにちは。
最近AWSの機械学習サービスであるSageMakerを使っています。
SageMakerは公式サンプルが充実していて、色々な機械学習を簡単に動かすことができます。
ただ、一通りサンプルを動かして、いざ自前の機械学習プログラムを動かそうと思うと、調べても資料があまりない！ということに気づきます。
試行錯誤しつつ自分で動かして、分からず結構苦労したので、そのノウハウをここにまとめました。

# 方法 
SageMaker Python SDKを使って少しコードを変えることで自前のコードをSageMaker上で動かすことができます。
ChainerやTensorflowなど各フレームワークごとにやり方が決まっています。
https://docs.aws.amazon.com/ja_jp/sagemaker/latest/dg/frameworks.html
今回はChainerのコードをSageMaker上で動かしてみました。

# 大まかな流れ
下記の3つを準備して実行します。
1)学習させるデータ 
2)学習を行うPythonのソースコード 
3)学習用のインスタンスを立ち上げ学習を起動するJupyter notebook 

Chainerのexampleにあるimagenetを使った画像分類のソースコード(詳細は後述)を例に今回は説明します。 
データセットはimagenetでは重いので、Caltech101を入力の型をあわせて使用しました。

## 1)学習させるデータ 
まずはCaltech101のデータセットの準備です。ここはSageMaker関係ない一般的な部分ですね。
下記リンクの「画像の用意、教師データとテストデータ」を参考にデータのダウンロード、リサイズ、meanファイルの作成を行います。 
https://qiita.com/rerere0101/items/fde1661df4a26f1d0626 

下記のようなディレクトリ構成になるはずです。

```
(親ディレクトリ)   
　├train.txt   
　├test.txt 
　├lobel.txt 
　└caltech101 
　　├accordion 
　　　　image_0001.jpg 
　　　　… 
　　├airplanes 
　　　　image_0001.jpg 
　　　　　… 
　　…
```

～～～
(下記は本データセットをSageMaker上で使用するときの特有の手順)
train.txt, test.txtは画像パスを下記のように変更します。
train.txtの中身 

```
　/opt/ml/input/data/train/caltech101/accordion/image_0001.jpg 0 
　/opt/ml/input/data/train/caltech101/accordion/image_0002.jpg 0 
```
test.txtの中身 

```
　/opt/ml/input/data/test/caltech101/accordion/image_0043.jpg 0 
　/opt/ml/input/data/test/caltech101/accordion/image_0044.jpg 0 
```

★注意点 
　<画像のパス> <正解ラベル> 
　という並びになりますが、データの呼び出しはS3からSageMakerインスタンスに呼び出された後に行われるため、パスはSageMakerインスタンスに置かれたときのパスを指定します。
　 
　S3に置いた画像データセットは/opt/ml/input/data/以下にコピーされます。
　学習用の画像は/opt/ml/input/data/train, 評価用の画像は/opt/ml/input/data/test以下に置かれます。

　SageMakerインスタンスにどのようにコピーされるかの構成はこちらを参照ください。
　 http://acro-engineer.hatenablog.com/entry/2018/09/05/120000 
～～～


このセット(train.txt, test.txt, caltech101ディレクトリ)をS3に配置してください。
  
## 2)学習を行うPythonのソースコード 
Chainerのexampleにある下記のソースコードを使用してSageMaker対応を行いました。
https://github.com/chainer/chainer/blob/master/examples/imagenet/train_imagenet.py 
名前はtrain_imagenet.pyですが、入力の型を合わせることによって、imagenet以外のデータセットでも使用することができます。
ただし、二箇所変更が必要です。

★変更箇所① S3のパスを学習を起動するJupyter Notebook側で指定するので受け口を作ります。
　下記をargparseで指定しているところに追記します。

```Python
    parser.add_argument('--output_data_dir', type=str, default=os.environ['SM_OUTPUT_DATA_DIR']) 
    parser.add_argument('--model-dir', type=str, default=os.environ['SM_MODEL_DIR']) 
    parser.add_argument('--train', type=str, default=os.environ['SM_CHANNEL_TRAIN']) 
    parser.add_argument('--test', type=str, default=os.environ['SM_CHANNEL_TEST']) 
```

次の引数を指定できるようにします。
・output_data_dir グラフ等生成物の出力ディレクトリ
・model-dir モデルの出力ディレクトリ
・train train画像のディレクトリ
・test test画像のディレクトリ

これに伴い、ソースコード内で同様の働きをしているもの(元々ソースコード内にあるtrainやval等)は書き換えます。
     
★変更箇所② データセットを指定する部分を変更します。
最初にargs.trainを呼び出す前(165行目より前)、に下記を入れます。

```Python
    # data setting 
    train = args.train + "/train.txt" 
    test = args.test + "/test.txt" 
```

学習を起動するJupyter NotebookではS3のバケットまでしか指定できないので、それ以降のファイル指定・操作は ソースコード側で行う必要があります。 
ソースコード内でargs.trainやargs.test(元々はargs.val)で渡しているものはそれぞれtrain, testに書き換えましょう。
     
## 3)学習用のインスタンスを立ち上げ学習を起動するJupyter notebook 
　https://github.com/awslabs/amazon-sagemaker-examples/blob/master/sagemaker-python-sdk/chainer_cifar10/chainer_single_machine_cifar10.ipynb 
　上記を参考に作成します。 

学習用のノートブックインスタンスには、以下のように配置します。
呼び出される側のプログラムはすべてsrcフォルダの中に入れておいてください。

```
学習起動Jupyter notebook 
src/
　└学習を行うPythonのソースコード
```

学習起動Jupyter notebookには下記のように書いていき、順に実行していきます。

sagemaker.session()の作成と、ロールの取得を行います。

```Python
import sagemaker
from sagemaker import get_execution_role
sagemaker_session = sagemaker.Session()

# Get a SageMaker-compatible role used by this Notebook Instance.
role = get_execution_role()
```

バケットとその後のパスを指定します。
imagenet_sagemaker以下に学習用データを置きました。

```python
bucket = '<your-bucket-name>'
prefix = 'sagemaker/imagenet-chainer'
S3_INPUT_PATH = 's3://{0}/{1}/'.format(bucket, prefix)
print(S3_INPUT_PATH)
```

estimaterの作成を行います。
呼び出すプログラムをentry_pointで、プログラムを入れたフォルダをsource_dirで指定しました。
source_dirで指定したフォルダがそのままSageMakerインスタンスにコピーされます。
(なので、ソースコード以外にもSageMakerインスタンスにコピーしたいものがあればsource_dirに入れておけば、一緒にコピーすることができます。)
train_imagenet.py内で元々指定するようになっている引数はhyperparametersで指定します。

```python
import subprocess
from sagemaker.chainer.estimator import Chainer

instance_type = 'ml.c4.xlarge'

chainer_estimator = Chainer(entry_point='train_imagenet.py', role=role, 
                            source_dir="src", 
                            train_instance_count=1,  
                            train_instance_type=instance_type, 
                            framework_version='5.0.0', 
                            hyperparameters={'arch': "mobilenet"}) 
```

学習を実行します。

```python
train_input = "s3://20190225-sagemaker/sagemaker/imagenet-chainer/" 
test_input = "s3://20190225-sagemaker/sagemaker/imagenet-chainer/" 
chainer_estimator.fit({'train': train_input, 'test': test_input}) 
```

これで自前の機械学習コードをSageMaker上で動かすことができました。

# ポイント 
①データはすべてS3にあげる。パスを指定する場合はSageMakerインスタンスにコピーされた後の構成を意識する 
②学習を行うソースコードはSageMakerの仕様に合わせる。
③ノートブックインスタンスからSageMakerインスタンスに送る学習用ソースコードは別ディレクトリにまとめておく。

# まとめ
自前のソースコードをSageMaker上でPython SDKを用いて動かしてみました。
SageMaker特有の仕様もあり、最初は分からない部分もありましたが、分かってしまえば簡単に実行できるものだな、と思います。
この記事ではChainerを例に説明しましたが、ポイントを押さえて書くことで、他のフレームワークでも応用できると思います。