エンジニア目線で始める Amazon SageMaker Training ①機械学習を使わないはじめての Training Job

## 記事一覧

https://qiita.com/kazuneet/items/294357a2214ac733f6f1

https://qiita.com/kazuneet/items/caa887826e2c33309e0b

https://qiita.com/kazuneet/items/62edfa445e526eb322d6

https://qiita.com/kazuneet/items/bba523109f5fac0f410b

## Amazon SageMaker Training コワクナイヨ
皆様、機械学習してますか？
してる方、Amazon SageMaker Training 使ってますか？
使いましょう。使いましょう。使いこなすとめっちゃ便利です。試行錯誤の回数がただの Notebook に比べて10倍くらい増えます。（自分の感覚+ポジショントーク）
でもどうやって使うん？という声に答える記事にしていきます。

## 結局 Amazon SageMaker Training って何をしてくれるの？
AWS のホームページに行くといろいろ抽象的かつコンセプチュアルなことが書いてあり、それはその通りなんですが、結局何をどうやって始めればいいの？というのが迷子になりがちです。
個人の意見ですが、Amazon SageMaker Training とは、①用意したコードを②用意したデータと③用意した環境で実行してくれ、④結果を自動で保存してくれる、バッチ処理サービスです。ただし、1-4はすべて必須というわけではない（というのが更に理解を難しくします）です。

この記事では裏側の動作とかについては一切触れずに、どうやって動かすのか、にフォーカスして記載していきます。
そこらへんを知りたい方はこちらの記事をご参照ください。
[[入門] 結局Amazon SageMakerは学習の時に何をしているのか？ - Qiita](https://qiita.com/triwave33/items/0a39e90068c57f1d2753)


**※この記事では機械学習のコードは一切出ません**

## Hello SageMaker Training !(用意したコードの実行)

まずは 「Amazon SageMaker Training とは、①用意したコードを ~~②用意したデータと③用意した環境で~~実行してくれ~~、 ④結果を自動で保存してくれ~~る、バッチ処理サービスです。」を体感してみましょう。

なにはともあれ実行してみましょう。

※以降、SageMaker Notebook/SageMaker Studio での実行を前提としています。手元のPCでも実行できますが、権限周りの設定をしてください。SageMaker Notebookの場合はカーネルは `conda_python3`、SageMaker Studio の場合は `Data Science` を使います。インスタンスは `ml.t3.medium` などで十分です。

※コードはすべて GitHub にあるので、手元にcloneして使ってみてください。

https://github.com/kazuhitogo/sagemaker-training-tutorial

### (手元のPCの人のみ)必要なライブラリのインストール
まずはSageMakerを操作するための SageMaker SDK をインストールしましょう。Notebook や Studio の人もバージョンアップしておくと最新の機能を使えます。

```bash:1_hello_sagemaker_training.ipynb
pip install sagemaker -U
```
インストール後にモジュールが import できるか確認します。

```python:1_hello_sagemaker_training.ipynb
import sagemaker
print(sagemaker.__version__)
```
```text:実行結果
2.74.0
```
バージョンが表示されれば成功です。以降のコードは `2.74.0` で動かしています。

### 書いたコードを実行させてみる(機械学習で言うと学習コードの実行)

まずは手元でコードを用意してみます。

```python:./src/1-1/hello_sagemaker_training.py
print('Hello SageMaker Training')
exit()
```

初心者向けの Python の教科書で出てくるような簡単なコードですね。これがわからない人はこの先に進めないので回れ右してお帰りください。（いらない煽り）

さて、このコードを SageMaker Training サービスを利用して実行してみましょう。

```python:1_hello_sagemaker_training.ipynb
from sagemaker.tensorflow import TensorFlow

estimator = TensorFlow(
    entry_point='./src/1-1/hello_sagemaker_training.py',
    py_version='py38', 
    framework_version='2.6.0',
    instance_count=1,
    instance_type='ml.m5.xlarge',
    role=sagemaker.get_execution_role()
)
estimator.fit()
```
数分待つと以下の出力を得られます。

```text:実行結果
(前略、結構な量を省略で、最後あたりまで行きます)
Hello SageMaker Training
(後略)
```

「Amazon SageMaker Training とは、①用意したコードを ~~②用意したデータと③用意した環境で~~実行してくれ、 ~~④結果を自動で保存してくれ~~る、バッチ処理サービスです。」を体感できました。そもそも機械学習である必要もありません。（もちろん機械学習のトレーニングを意識して作られていますが）
ノートブックで一体何をやったのかコードをコメントを入れてちょっと見てみましょう。

```python
# 実行環境の指定
estimator = TensorFlow(
    # 用意したコードを、にあたる部分
    entry_point='./src/1-1/hello_sagemaker_training.py', # 
    py_version='py38', 
    framework_version='2.6.0',
    instance_count=1,
    instance_type='ml.m5.xlarge',
    role=sagemaker.get_execution_role()
)
# 実行
estimator.fit()
```
今回は用意したコードを、の部分を体感してますが、その部分を体現する場所は、`entry_point` 引数で、作ったコードを指定しています。ほかは最後の行以外すべて実行環境の指定です。

環境の指定した内容は以下のとおりです。

* コンテナ : [TensorFlow](https://sagemaker.readthedocs.io/en/stable/frameworks/tensorflow/sagemaker.tensorflow.html#tensorflow-estimator) クラスを利用して、`py_version=py3.8`かつ`framework_version=2.6.0`を指定することで、Pythonのバージョンが3.8でTensorFlowのバージョンが2.6.0をプリインストールしたコンテナイメージで実行することを指定しています。このコンテナイメージは AWS が管理するイメージで、ユーザーは管理・運用する必要はありません。
* インスタンスの数 : `instance_count=1` を指定することで動かすインスタンスの台数を決めています。機械学習で分散学習するときは複数台使いますが、今回はもちろん 1 台で十分です。
* インスタンスタイプ : `instance_type=ml.m5.xlarge`を指定することで、コンテナの実行環境に m5.xlarge を使いなさい、という指定をしています。

このようにコードを用意して、環境を指定(=用意する必要はありません）して、`fit()` することでコードを実行してくれることがわかりました。また使った環境（インスタンス）は処理が終わると自動で停止するため、不必要な課金もされないです。

### 他の環境を指定してみる
何も言わずにPython 3.8,TensorFlow 2.6.0という環境を指定しましたが、他の環境は指定できないのか？という疑問があります。もちろんできます。結果は省略しますが、同様の結果を得られます。

#### PyTorch 1.9.1, Python 3.8, m5.xlarge の場合

[PyTorch Estimator の詳細はこちら](https://sagemaker.readthedocs.io/en/stable/frameworks/pytorch/sagemaker.pytorch.html#pytorch-estimator)

```python:1_hello_sagemaker_training.ipynb
from sagemaker.pytorch import PyTorch
estimator = PyTorch(
    entry_point='./src/1-1/hello_sagemaker_training.py',
    py_version='py38', 
    framework_version='1.9.1',
    instance_count=1,
    instance_type='ml.m5.xlarge',
    role=sagemaker.get_execution_role()
)
estimator.fit()
```

#### MXNet 1.8.0 Python 3.7, m4.xlarge の場合

[MXNet Estimator の詳細はこちら](https://sagemaker.readthedocs.io/en/stable/frameworks/mxnet/sagemaker.mxnet.html#mxnet-estimator)

```python:1_hello_sagemaker_training.ipynb
from sagemaker.mxnet import MXNet
estimator = MXNet(
    entry_point='./src/1-1/hello_sagemaker_training.py',
    py_version='py37', 
    framework_version='1.8.0',
    instance_count=1,
    instance_type='ml.m4.xlarge',
    role=sagemaker.get_execution_role()
)
estimator.fit()
```

#### Scikit-Learn 0.23-1 Python 3.7, c5.xlarge の場合

[Scikit Learn Estimator の詳細はこちら](https://sagemaker.readthedocs.io/en/stable/frameworks/sklearn/sagemaker.sklearn.html#scikit-learn-estimator)

```Python:1_hello_sagemaker_training.ipynb
from sagemaker.sklearn import SKLearn
estimator = SKLearn(
    entry_point='./src/1-1/hello_sagemaker_training.py',
    py_version='py3', 
    framework_version='0.23-1',
    instance_count=1,
    instance_type='ml.c5.xlarge',
    role=sagemaker.get_execution_role()
)
estimator.fit()
```

このように AWS が管理しているコンテナとインスタンスを自由に選んで実行することができます。
では環境一覧はどこを見ればいいのか、という話が出てきます。

TensorFlow/PyTorch/MXNet のコンテナ一覧は [GitHub](https://github.com/aws/deep-learning-containers/blob/master/available_images.md) にて公開されています。
Job Type が training になっているものを使用できるので、`framework_version` の指定と `py_version` の指定に使えます。また、Dockerfile も GitHub にて公開されているので中身を見たい場合は併せて参照してください。

Scikit-Learn のコンテナも同様に[ソースコード](https://github.com/aws/sagemaker-scikit-learn-container)が公開されているので参考にしてください。

次にどのコンテナを使ったのか、URI を確認する方法と、URI を直接指定して実行する方法に触れておきます。

最後に実行したのは Scikit-Learn コンテナなのでこの URI を取り出します。estimator 変数に URI が残ってます。以下のコードで URI 他実行した Job の詳細を取得できます。

```python:1_hello_sagemaker_training.ipynb
estimator.latest_training_job.describe()
```

```json:実行結果
{
  (略)
  'sagemaker_program': '"hello_sagemaker_training.py"',
  (略)
  'sagemaker_submit_directory': '"s3://sagemaker-{REGION}-{ACCOUNT_ID}/sagemaker-scikit-learn-{YYYY}-{MM}-{DD}-{HH}-{MI}-{SS}-{mmm}/source/sourcedir.tar.gz"',
  (略)
  'TrainingImage': '257758044811.dkr.ecr.us-east-2.amazonaws.com/sagemaker-scikit-learn:0.23-1-cpu-py3',
  (略)
}
```
コンテナイメージの URI を見る前に、`sagemaker_program` と `sagemaker_submit_directory` を見ましょう。実行したコードが `sourcedir.tar.gz` に固められて配置され、また実行コードが `hello_sagemaker_training.py` に残っていることがわかります。この情報は AWS マネジメントコンソールでも、他の AWS SDK (boto3など)でも確認できます。実行したときの状況がすべて記録されているので、機械学習であの時なにやったっけ？というときに遡れるのがとても楽でいいですね。

話を戻します。`TrainingImage` キーにコンテナイメージの URI が乗ってます。先程は `framework_version` や `py_version` を指定しましたが、直接指定することも可能です。

```python:1_hello_sagemaker_training.ipynb
from sagemaker.sklearn import SKLearn
estimator = SKLearn(
    entry_point='./src/1-1/hello_sagemaker_training.py',
    image_uri = estimator.latest_training_job.describe()['AlgorithmSpecification']['TrainingImage'],
    instance_count=1,
    instance_type='ml.c5.xlarge',
    role=sagemaker.get_execution_role()
)
estimator.fit()
```
先程の `describe()` で得られるコンテナイメージの URI を `image_uri` 引数に指定して実行しました。全く同じ結果が得られます。Bring Your Own Container する(ECR に自分でコンテナイメージをビルドして pushしたものを使う)際は `image_uri` を指定して実行するので覚えておきましょう。

以後のコードはどの Estimator でも実行可能です。代表して TensorFlow Estimator を使用します。

## 用意したデータを SageMaker Training 内で使う(機械学習で言うと学習データの利用)
次は、「Amazon SageMaker Training とは、①用意したコードを②用意したデータと ~~③用意した環境で~~ 実行してくれ ~~、④結果を自動で保存してくれ~~ る、バッチ処理サービスです。」を体感しましょう。

### 入力データ
こんなデータを用意しました。

```text:./data/1-2-1/calc.txt
3+4
4-2
5*1
6/2
```

算数の計算式が書いてあります。これらの結果を SageMaker Training で出力してみましょう。

まず、そもそもどうやって Python で解くのか、というコードを念の為。

```python:1_hello_sagemaker_training.ipynb
with open('./data/1-2-1/calc.txt','rt') as f:
    input_text_lines = f.read()
for input_text in input_text_lines.split('\n'):
    print(eval(input_text))
```

```text:実行結果
7
2
5
3.0
```
1 行ずつ読み出して `eval` で式として評価するだけですね。簡単です。

### ファイルを S3 にアップロードする

さて、同じことを SageMaker Training でやるには、`calc.txt` を SageMaker Training のインスタンスに配置する必要があります。もちろん SageMaker Training にはその仕組があります。
その仕組を使うには予め S3 にデータを配置する必要があります。AWS CLI などで送り込んでもいいのですが、SageMaker SDK にもその機能があるので、せっかくなので使ってみましょう。

```python:1_hello_sagemaker_training.ipynb
import sagemaker
input_s3_uri = sagemaker.session.Session().upload_data(path='./data/1/calc.txt', bucket=sagemaker.session.Session().default_bucket(), key_prefix='training/1-2-1')
print(input_s3_uri)
```
```text:実行結果
s3://sagemaker-{REGION}-{ACCOUNT_ID}/training/1-2-1/calc.txt
```

[upload_data()](https://sagemaker.readthedocs.io/en/stable/api/utility/session.html?highlight=upload_data#sagemaker.session.Session.upload_data) を使うことで S3 にアップロードすることができます。また返り値に格納先の S3 の URI が入ります。この URI を利用して Training インスタンスにデータを送り込みます。

### SageMaker Training 内に S3 のデータを送りこむ
まずはコードから行きます。

```python:./src/1-2-1/calc.py
import os
input_dir = os.environ.get('SM_CHANNEL_TRAINING')
input_txt_path = os.path.join(input_dir,os.listdir(input_dir)[0])
with open(input_txt_path,'rt') as f:
    input_text_lines = f.read()
for input_text_line in input_text_lines.split('\n'):
    print(eval(input_text_line))
exit()
```

### S3 にあるファイルを使った Training Job を実行

Training Job を実行します。

```python:1_hello_sagemaker_training.ipynb
from sagemaker.tensorflow import TensorFlow
estimator = TensorFlow(
    entry_point='./src/1-2-1/calc.py',
    py_version='py38', 
    framework_version='2.6.0',
    instance_count=1,
    instance_type='ml.m5.xlarge',
    role=sagemaker.get_execution_role()
)
estimator.fit(input_s3_uri)
```

```text:実行結果
(略)
SM_CHANNEL_TRAINING=/opt/ml/input/data/training
(略)
7
2
5
3.0
(後略)
```

Training Job の呼び出し時に、単純にコードを実行してたときは、`estimator.fit()` と `fit()` に引数がなかったのに対し、今回のようにデータを連携させるときは、`estimator.fit(input_s3_uri)` と `fit()` の引数に S3 の URI が入りました。これによって Training インスタンスに S3 に配置したデータが転送されます。

では、 Training インスタンスのどこに行くのかですが、答えは実行結果にある`SM_CHANNEL_TRAINING=/opt/ml/input/data/training` です。`SM_CHANNEL_TRAINING` は Training インスタンスの環境変数でこの値に格納されているディレクトリの直下にデータが配置されます。
ですので、実行コードの 2 行目の `input_dir = os.environ.get('SM_CHANNEL_TRAINING')` でそのディレクトリを取得し、 3 行目の `input_txt_path = os.path.join(input_dir,os.listdir(input_dir)[0])` でそのディレクトリにある `calc.txt` を取得しています。

他にも環境変数は設定されていますが詳細はこちらの[Environment variables](https://github.com/aws/sagemaker-training-toolkit/blob/master/ENVIRONMENT_VARIABLES.md)を参照ください

### ディレクトリ配下にあるファイルを丸ごと使う Training Job

さて、前回は 1 ファイルだったからこれで良いですが、ディレクトリ配下にある複数のデータを使いたい場合はどうすればいいのでしょうか？その時は単純に S3 の prefix を指定するだけでよいです。

こんなデータを用意しました。

```text:./data/1-2-2/calc1.txt
2+2
3-1
```

```text:./data/1-2-2/calc2.txt
5*1
16/2
```

次にコードです。

```python:./src/1-2-2/calc.py
import os
input_dir = os.environ.get('SM_CHANNEL_TRAINING')
for file_name in os.listdir(input_dir):
    input_txt_path = os.path.join(input_dir,file_name)
    with open(input_txt_path,'rt'):
        input_text_lines = f.read()
    for input_text_line in input_text_lines.split('\n'):
        print(eval(input_text_line))
exit()
```
最後に Training Job の実行です。

```python:1_hello_sagemaker_training.ipynb
from sagemaker.tensorflow import TensorFlow
input_s3_uri = sagemaker.session.Session().upload_data(path='./data/1-2-2/', bucket=sagemaker.session.Session().default_bucket(), key_prefix='training/1-2-2')
print(input_s3_uri)
estimator = TensorFlow(
    entry_point='./src/1-2-2/calc.py',
    py_version='py38', 
    framework_version='2.6.0',
    instance_count=1,
    instance_type='ml.m5.xlarge',
    role=sagemaker.get_execution_role()
)
estimator.fit(input_s3_uri)
```
```text:実行結果
(略)
SM_CHANNEL_TRAINING=/opt/ml/input/data/training
(略)
4
2
5
8.0
(略)
```
`upload_data()` はディレクトリをまるごと S3 にアップロードでき、その場合は返り値は S3 の URI の Prefix までを返します。そして、`fit()`にその S3 の URI の Prefix までを入力することで、Prefix 配下にあるファイルを丸ごと Training インスタンスに転送します。転送先は先程と同様、`SM_CHANNEL_TRAINING` という環境変数が定義しているディレクトリ(`/opt/ml/input/data/training`)に配置されるので、それを `os.listdir()` で取得して、ループさせれば OK です。


### 複数の Prefix にあるデータを使う Training Job
機械学習においては、train データ、validation データ、test データと分けたり、あるいは Cross Validation で k-fold してディレクトリ(=S3 にあったらPrefix)を k 個に分けて使うことも多いです。それらをまとめて Training インスタンスで使う仕組みもあります。

またデータを用意しました。

```text:./data/1-2-3/fold1/calc.txt
1+2
3+4
```

```text:./data/1-2-3/fold2/calc.txt
5+6
7+8
```

コードはこちらです。

```python:./src/1-2-3/calc.py
import os, json
channels = os.environ.get('SM_CHANNELS')
channels_list = json.loads(channels)
for channel in channels_list:
    input_dir = os.environ.get('SM_CHANNEL_' + channel.upper())
    for file_name in os.listdir(input_dir):
        input_txt_path = os.path.join(input_dir,file_name)
        with open(input_txt_path,'rt') as f:
            input_text_lines = f.read()
        for input_text_line in input_text_lines.split('\n'):
            print(eval(input_text_line))
exit()
```
実行してみます。

```python:1_hello_sagemaker_training.ipynb
import sagemaker
from sagemaker.tensorflow import TensorFlow
fold1_input_s3_uri = sagemaker.session.Session().upload_data(path='./data/1-2-3/fold1/', bucket=sagemaker.session.Session().default_bucket(), key_prefix='training/1-2-3/fold1')
fold2_input_s3_uri = sagemaker.session.Session().upload_data(path='./data/1-2-3/fold2/', bucket=sagemaker.session.Session().default_bucket(), key_prefix='training/1-2-3/fold2')
print(fold1_input_s3_uri, fold2_input_s3_uri)
estimator = TensorFlow(
    entry_point='./src/1-2-3/calc.py',
    py_version='py38', 
    framework_version='2.6.0',
    instance_count=1,
    instance_type='ml.m5.xlarge',
    role=sagemaker.get_execution_role()
)
estimator.fit({
    'fold1':fold1_input_s3_uri,
    'fold2':fold2_input_s3_uri,
})
```

```text:実行結果
s3://sagemaker-{REGION}-{ACCOUNT_ID}/training/1-2-3/fold1 s3://sagemaker-{REGION}-{ACCOUNT_ID}/training/1-2-3/fold2
(略)
SM_CHANNELS=["fold1","fold2"]
(略)
SM_CHANNEL_FOLD2=/opt/ml/input/data/fold2
SM_CHANNEL_FOLD1=/opt/ml/input/data/fold1
(略)
3
7
11
15
(略)
```

今までは `fit()` の引数に S3 URI の文字列を入れてましたが、
辞書型を入れてます。辞書型の値に S3 の URI を入れることで複数のデータソースを送り込むことができます。
では送り込んだ先は？というと、今までは`SM_CHANNEL_TRAINING`という環境変数でデフォルトの設定がされていましたが、
今回は`SM_CHANNEL_FOLD1`と`SM_CHANNEL_FOLD2`という、`SM_CHANNEL_`に辞書のキーを大文字にした形で入りました。
しかし実際に機械学習をする際はどんな CHANNEL のキーでデータが送られてくるか未知なことも多いです（ソースコードは同じで別のデータで使うときなど）。
そんなときのために、今回のジョブではどんな CHANNEL が含まれていますか？というのを `SM_CHANNELS` という環境変数で確認することができます。（今回は`["fold1","fold2"]`）
`SM_CHANNELS`の値を一つずつ取り出して大文字にして、`SM_CHANNEL_`にくっつけて使えば、すべてのCHANNELのデータをロードすることができます。

## 処理結果を保存する(機械学習で言うとモデルの保存)
次は 「Amazon SageMaker Training とは、①用意したコードを ②用意したデータと ~~③用意した環境で~~ 実行してくれ、 ④結果を自動で保存してくれる、バッチ処理サービスです。」を体感します。
機械学習の場合、トレーニングしてモデルを保存するときに使う機能で、自動で S3 に保存するようにします。

こんなデータを用意しました。

```text:./data/1-3/data1.csv
1,2
3,4
```

```text:./data/1-3/data2.csv
5,6
7,8
```
これらを縦に連結して、

```text
1,2
3,4
5,6
7,8
```
という csv ファイルを自動で S3 に保存してみます。

コードは以下の通りです。

```python:./src/1-3/concat.py
import os, json
channels = os.environ.get('SM_CHANNELS')
model_dir = os.environ.get('SM_MODEL_DIR')
channels_list = json.loads(channels)
output_txt = ''
for channel in channels_list:
    input_dir = os.environ.get('SM_CHANNEL_' + channel.upper())
    for file_name in sorted(os.listdir(input_dir)):
        input_txt_path = os.path.join(input_dir,file_name)
        with open(input_txt_path,'rt') as f:
            output_txt += f.read()+'\n'
with open(os.path.join(model_dir,'output.csv'),'wt') as f:
    f.write(output_txt)
print('processing completed')
exit()
```

実行してみます。

```python:1_hello_sagemaker_training.ipynb
import sagemaker
from sagemaker.tensorflow import TensorFlow
input_s3_uri = sagemaker.session.Session().upload_data(path='./data/1-3/', bucket=sagemaker.session.Session().default_bucket(), key_prefix='training/1-3')
estimator = TensorFlow(
    entry_point='./src/1-3/concat.py',
    py_version='py38', 
    framework_version='2.6.0',
    instance_count=1,
    instance_type='ml.m5.xlarge',
    role=sagemaker.get_execution_role()
)
estimator.fit(input_s3_uri)
```

```text:実行結果
(略)
    "hyperparameters": {
        "model_dir": "s3://sagemaker-{REGION}-{ACCOUND_ID}/tensorflow-training-{YYYY}-{MM}-{DD}-{HH}-{MI}-{SS}-{sss}/model"
    },
(略)
    "job_name": "tensorflow-training-{YYYY}-{MM}-{DD}-{HH}-{MI}-{SS}-{sss}",
(略)
SM_MODEL_DIR=/opt/ml/model
(略)
processing completed
(略)
```
このままだとわかりづらいので、先程使った `describe()` を使ってみます。

```python:./1_hello_sagemaker_training.ipynb
estimator.latest_training_job.describe()
```

```text:実行結果
{'TrainingJobName': 'tensorflow-training-{YYYY}-{MM}-{DD}-{HH}-{MI}-{SS}-{sss}',
(略))
 'ModelArtifacts': {'S3ModelArtifacts': 's3://sagemaker-{REGION}-{ACCOUNT_ID}/tensorflow-training-{YYYY}-{MM}-{DD}-{HH}-{MI}-{SS}-{sss}/output/model.tar.gz'},
 'HyperParameters': {'model_dir': '"s3://sagemaker-{REGION}-{ACCOUNT_ID}/tensorflow-training--{YYYY}-{MM}-{DD}-{HH}-{MI}-{SS}-{sss}/model"',
(略)
```
さて、まずは保存結果を確認してみましょう。`describe()` で入手できる `S3ModelArtifacts` キーに格納されている S3 の URI に保存したデータが格納されています。
ダウンロードして出力してみます。

```python:./1_hello_sagemaker_training.ipynb
import tarfile
prefix = estimator.latest_training_job.describe()['ModelArtifacts']['S3ModelArtifacts'].replace(f's3://{sagemaker.session.Session().default_bucket()}/','')
sagemaker.session.Session().download_data('./', sagemaker.session.Session().default_bucket(), key_prefix=prefix)
with tarfile.open('./model.tar.gz', 'r') as f:
    f.extractall()
```

```text:./output.csv
1,2
3,4
5,6
7,8

```

余計な改行が入ってますが（除外コード書くと長くなるので）、連結したデータが出来上がっていました。ではどうやってそれが S3 に転送されていたのか、という話です。
`SM_MODEL_DIR` という環境変数で定義された値(`/opt/ml/model`)のディレクトリに保存すると、処理が完了した際に自動的に S3 に保存される仕組みが入ってます。
ですので、今回はそのディレクトリに output.csv を配置することで S3 に自動で転送されました。また、転送する際に自動で tar.gz で圧縮する処理が入るため、`model.tar.gz` という形で保存されました。
さて、保存先は `describe()` で得られる `S3ModelArtifacts` キーの値に格納されますが、これも命名規則は決まっており、デフォルトでは SageMaker Training Job の `s3://{SageMakerのdefault bucket}/{job_name}/model/mode.tar.gz`に配置されます。また、SageMakerのdefault bucket は `sagemaker-{REGION}-{ACCOUNT_ID}`であり、job_name は {Training Job を実行したコンテナイメージの名前}-{処理を実行した年月日時分秒ミリ秒のYYYY-MM-DD-HH-MI-SS-sss}です。

もちろん job_name も S3 の転送先もある程度制御できます。同じ処理で変えてみましょう。

```python:./1_hello_sagemaker_training.ipynb
import sagemaker
from sagemaker.tensorflow import TensorFlow
input_s3_uri = sagemaker.session.Session().upload_data(path='./data/1-4/', bucket=sagemaker.session.Session().default_bucket(), key_prefix='training/1-3')
estimator = TensorFlow(
    base_job_name = 'concat', # 追加
    output_path = f's3://{sagemaker.session.Session().default_bucket()}/concat-job', # 追加
    entry_point='./src/1-3/concat.py',
    py_version='py38', 
    framework_version='2.6.0',
    instance_count=1,
    instance_type='ml.m5.xlarge',
    role=sagemaker.get_execution_role()
)
estimator.fit(input_s3_uri)
estimator.latest_training_job.describe()
```

```text:
(略)
{'TrainingJobName': 'concat-2022-02-04-03-20-01-005',
(略)
 'ModelArtifacts': {'S3ModelArtifacts': 's3://{SageMakerのdefault bucket}/concat-job/concat-{処理を実行した年月日時分秒ミリ秒のYYYY-MM-DD-HH-MI-SS-sss},
(略)
```
`base_job_name` と `output_path` の引数を使うことで job_name とデータの転送先をコントロールできました。

## 実行前にライブラリを追加する
さて、今までAWS が管理しているコンテナを使ってきましたが、コンテナに入っていないライブラリをある時使いたくなったとします。その場合どうすればよいのか、を紹介します。本来は「 ~~①用意したコードを②用意したデータと~~ ③用意した環境 ~~で実行してくれ、④結果を自動で保存してくれる、バッチ処理サービスです。~~」の部分で、自身で用意したコンテナに予めインストールすべきですが、コンテナの準備ほど大掛かり（この記事ではBring Your Own Container を扱わず、次の記事を予定しています）にやらずにインストールして使う方法があります。あるいは自作モジュール( `model.py` とか作って `train.py` と分離させたりしますよね)を使う場合どうすればいいのかも併せて紹介します。

機械学習のトレーニングコードではあまり使われない、`BeautifulSoup`ライブラリを使いたくなったとしましょう。
こんなコードを用意しました。

```python:./src/1-4/my_module.py
def version_check(module):
    print(f' {module.__name__} version is {module.__version__}')
    return None
```

```python:./src/1-4/bs4_version_check.py
import bs4
print(f'BeautifulSoup version is {bs4.__version__}.')
exit()
```

これをこのまま実行すると `beautifulsoup4` をインストールしていないため、エラーで落ちますので、もう 1 ファイル追加します。

```python:./src/1-4/requirements.txt
beautifulsoup4==4.9.3
```

実行してみます。

```python:./1_hello_sagemaker_training.ipynb
from sagemaker.tensorflow import TensorFlow
estimator = TensorFlow(
    entry_point='bf4_version_check.py',
    source_dir='./src/1-4',
    py_version='py38', 
    framework_version='2.6.0',
    instance_count=1,
    instance_type='ml.m5.xlarge',
    role=sagemaker.get_execution_role()
)
estimator.fit()
```

```text:実行結果
(略)
/usr/local/bin/python3.8 -m pip install -r requirements.txt
Collecting beautifulsoup4==4.9.3
Downloading beautifulsoup4-4.9.3-py3-none-any.whl (115 kB)
Collecting soupsieve>1.2
Downloading soupsieve-2.3.1-py3-none-any.whl (37 kB)
Installing collected packages: soupsieve, beautifulsoup4
Successfully installed beautifulsoup4-4.9.3 soupsieve-2.3.1
(略)
bs4 version is 4.9.3.
(略)
```

`beautifulsoup4` の指定したバージョンがインストールされ、使えたことが確認できます。
動作させるには 

1. `requirements.txt` を用意して、インストールしたライブラリを記載（バージョンは省略可、`pip install -r requirements.txt` するときと同じ書き方)し、
2. estimator の `source_dir` で指定したディレクトリの直下に `requirements.txt` と自作モジュール(my_module.py)実行コード(今回は `bs4_version_check.py`)を配置し、
3. entry_point にはファイル名だけ(今回は `bf4_version_check.py`、今までは `./src/1-5/concat.py`など相対パスを入力していた)を指定

します。

仕組みとしては、`source_dir` で指定したディレクトリの中身を Training インスタンスに転送してくれ、Python の処理が始まる前に、`/usr/local/bin/python3.8 -m pip install -r requirements.txt` によってインストール処理が走ります。

また、この `source_dir` 引数を使うと、`source_dir` で指定したディレクトリ配下に保存した 自作の `.py` ファイルや、 `pip install {library} -t {source_dir}`のようにディレクトリ指定してインストールしたライブラリも転送してくれます。
Training インスタンスが VPC 内で立ち上がり、インターネットに出られない場合は、この方法でライブラリを持ち込むことが可能です。

## 処理を動的に変える(機械学習で言うとハイパーパラメータの外部からの入力)
機械学習でトレーニングする際、ソースコードを変えずに処理を動的に変えたいケースがあります。
具体的にはハイパーパラメータチューニングをする時などに、epoch 数や batch size、filter 数や、layer 数を変える、などいろいろ変えたいものがありますが、都度ソースコードをいじるのは大変です。
Training Job を呼び出し時にこれらを引数として受け取って実行内容に反映する方法があります。
コードを用意して実行してみました。

```python:./src/1-5/switch_process_by_hyperparameter.py
import os, argparse
parser = argparse.ArgumentParser()
parser.add_argument('--first-num', type=int)
parser.add_argument('--second-num', type=int)
parser.add_argument('--operator', type=str)
parser.add_argument('--model_dir', type=str)
args = parser.parse_args()
if args.operator == 'p':
    print(f'The answer is {args.first_num + args.second_num}')
elif args.operator == 'm':
    print(f'The answer is {args.first_num - args.second_num}')
exit()
```

```python:./1_hello_sagemaker_training.ipynb
from sagemaker.tensorflow import TensorFlow
estimator = TensorFlow(
    entry_point='./src/1-5/switch_process_by_hyperparameter.py',
    py_version='py38', 
    framework_version='2.6.0',
    instance_count=1,
    instance_type='ml.m5.xlarge',
    role=sagemaker.get_execution_role(),
    hyperparameters={
        'first-num':5,
        'second-num':2,
        'operator':'m'
    }
)
estimator.fit()
```

```text:実行結果
(略)
SM_HPS={"first-num":5,"model_dir":"s3://sagemaker-{REGION}-{ACCOUNT_ID}/tensorflow-training-{YYYY}-{MM}-{DD}-{HH}-{MI}-{SS}-{mmm}/model","operator":"m","second-num":2}
(略)
SM_HP_FIRST-NUM=5
SM_HP_MODEL_DIR=s3://sagemaker-{REGION}-{ACCOUNT_ID}/tensorflow-training-{YYYY}-{MM}-{DD}-{HH}-{MI}-{SS}-{mmm}/model
SM_HP_OPERATOR=m
SM_HP_SECOND-NUM=2
(略)
/usr/local/bin/python3.8 switch_process_by_hyperparameter.py --first-num 5 --model_dir s3://sagemaker-{REGION}-{ACCOUNT_ID}/tensorflow-training-{YYYY}-{MM}-{DD}-{HH}-{MI}-{SS}-{mmm}/model --operator m --second-num 2
The answer is 3
(略)
```


estimator 内に `hyperparameters` という引数が追加され、辞書形式のデータを入力しています。`hyperparameters` で入力した内容が、Python コードを呼び出す際のコマンドライン引数として入ります。（他にも環境変数としても入りますが、ここではコマンドライン引数を使います。環境変数から取得しても OK です。）
実は今までもコマンドライン引数に `--model_dir` はあったのですが、使っておりませんでした。
今回はこのコマンドライン引数を `argparse` で受けて、`--first-num` と `--second-num` を、`operator` が `p` だったら足し算を、`m`だったら引き算をして出力する、という処理を `./src/1-7/switch_process_by_hyperparameter.py` で書いています。(--model-dirは受け取らないとエラーが発生するので受け取っているが使っていない)

このように呼び出し時に入力した値をコードで引き受ける仕組みも SageMaker Training は持っているので、ハイパーパラメータをチューニングする際に利用してください。

## (番外編)ログの確認
`fit()`した際にノートブックに出力された内容は、CloudWatch Logs に転送されています。セッションが切れたりしてログが見えなくなったとしたとしても安心してください。
最後に行った Training Job で確認してみましょう。（実際はマネジメントコンソールで確認したほうが楽です）

```python:./1_hello_sagemaker_training.ipynb
import boto3
logs = boto3.client('logs')
log_group_name = '/aws/sagemaker/TrainingJobs'
latest_logstream_name = logs.describe_log_streams(
    logGroupName=log_group_name,
    orderBy='LastEventTime',
    descending=True
)['logStreams'][0]['logStreamName']
logs.get_log_events(
    logGroupName=log_group_name,
    logStreamName=latest_logstream_name,
)
```

```text:実行結果
(略)
  {'timestamp': {UNIX時刻},
   'message': '/usr/local/bin/python3.8 switch_process_by_hyperparameter.py --first-num 5 --model_dir s3://sagemaker-{REGION}-{ACCOUNT_ID}/tensorflow-training-{YYYY}-{MM}-{DD}-{HH}-{MI}-{SS}-{mmm}/model --operator m --second-num 2',
   'ingestionTime': {UNIX時刻}},
  {'timestamp': {UNIX時刻},
   'message': 'The answer is 3',
   'ingestionTime': {UNIX時刻}},
(略)
```


他、Training インスタンスのメモリや CPU の使用率は CloudWatch メトリクスに残るので、必要に応じて使いましょう。

## まとめ

1. SageMaker Training はソースコードを書いて Job を呼び出せばコード(機械学習ならば学習コード)を実行できる
1. コードを実行するのにデータが必要であれば(機械学習ならば学習データ）、`fit()` の引数で使うデータを指定して、SM_CHANNEL_XXの環境変数の値のディレクトリに配置されるのでそれを使う
1. 終わった後の成果物を保存するならば(機械学習ならモデル)、SM_MODEL_DIRの環境変数の値のディレクトリに保存すれば自動的に S3 に転送される

というとっても便利なサービスです。
続編はいつになるかわかりませんが、
1. ①用意したコードを②用意したデータと③用意した環境で実行してくれ、④結果を自動で保存してくれる、バッチ処理サービスです。の全部盛り(Bring Your Own Container)
2.  ~~①用意したコードを~~ ②用意したデータ ~~と③用意した環境~~ で実行してくれ、④結果を自動で保存してくれる、バッチ処理サービスです。(Built in Algorithm/Auto Pilot)
3. その他便利機能

のどこかをやろうと思います。(2022/2/16 追記: やりませんでした)Thank You!