自前のKerasコードをSageMakerコードに変換する

前回に引き続き、MLPをKerasで構築した場合、SageMaker用にどう変換するのか、記載していく。

Neural Netの場合は、
・変数のスケーリング[0,1]が必要
・欠損値の補完が必要
・カテゴリ変数はone-hot-encodingする
等の前処理が必要になる。

# 自前のKeras

```py
from keras.layers import Dense, Dropout
from keras.models import Sequential
from sklearn.metrics import log_loss
from sklearn.preprocessing import StandardScaler

# データのスケーリング
scaler = StandardScaler()
tr_x_scale = scaler.fit_transform(tr_x)
va_x_scale = scaler.transform(va_x)
test_x_scale = scaler.transform(test_x)

# ニューラルネットモデルの構築
model = Sequential()
model.add(Dense(256, activation='relu', input_shape=(tr_x_scale.shape[1],)))
model.add(Dropout(0.2))
model.add(Dense(256, activation='relu'))
model.add(Dropout(0.2))
model.add(Dense(1, activation='sigmoid'))

model.compile(loss='binary_crossentropy',
              optimizer='adam', metrics=['accuracy'])

# 学習の実行
# バリデーションデータもモデルに渡し、学習の進行とともにスコアがどう変わるかモニタリングする
batch_size = 128
epochs = 10
history = model.fit(tr_x_scale, tr_y,
                    batch_size=batch_size, epochs=epochs,
                    verbose=1, validation_data=(va_x_scale, va_y))

# バリデーションデータでのスコアの確認
va_pred = model.predict(va_x_scale)
score = log_loss(va_y, va_pred, eps=1e-7)
print(f'logloss: {score:.4f}')
```

# SageMaker用コードに変換

```py
# S3に保存するためにデータを正解ラベルを1つにまとめる
joined_tr = np.insert(tr_x_scale, 0, tr_y, axis=1)
joined_va = np.insert(va_x_scale, 0, va_y, axis=1)

# ローカルにcsv作成(ヘッダなし)
pd.DataFrame(joined_tr).to_csv('train.csv', header=False, index=False)
pd.DataFrame(joined_va).to_csv('validation.csv', header=False, index=False)

### S3にアップ
import sagemaker
from sagemaker import get_execution_role

# default_bucket=None　を任意で指定することも可能
# https://sagemaker.readthedocs.io/en/stable/session.html
sagemaker_session = sagemaker.Session()

# バケット: sagemaker-<region>-<アカウントID> に保存される。(上記で指定しなかったため)
input_train = sagemaker_session.upload_data(path='train.csv', key_prefix='sagemaker/keras-breast-cancer')
input_validation = sagemaker_session.upload_data(path='validation.csv', key_prefix='sagemaker/keras-breast-cancer')

from sagemaker.tensorflow import TensorFlow

batch_size = 128
num_classes = 1 # 0 or 1 のときは1???
epochs = 10

estimator = TensorFlow(entry_point='keras_breast_cancer_script.py',
                       role=role,
                       framework_version='1.12.0',
                       hyperparameters={
                           'batch-size' : batch_size,
                           'epochs' : epochs,
                           'num-classes' : num_classes
                       },
                       train_instance_count=1,
                       train_instance_type='ml.m5.xlarge',
                       script_mode=True,
                       py_version='py3')

estimator.fit({'train' : input_train, 'validation' : input_validation})

### エンドポイントのデプロイ（予測を実施するサーバを作成）
nn_predictor = estimator.deploy(initial_instance_count = 1, instance_type = 'ml.m4.xlarge')

# 予測
pred = nn_predictor.predict(test_x_scale)
```

# 別途、学習用のpythonファイルを書く必要がある。
上記のestimator宣言で、ファイルを指定している。

```keras_breast_cancer_script.py
import argparse
import os
import numpy as np
import pandas as pd
import tensorflow as tf
import keras

from keras.layers import Dense, Dropout
from keras.models import Sequential
from keras.optimizers import RMSprop
from keras import backend as K

if __name__ == '__main__':
    parser = argparse.ArgumentParser()

    # ハイパーパラメータとして与える値を引数で受け取る
    parser.add_argument('--batch-size', type=int, default=128)
    parser.add_argument('--epochs', type=int, default=10)
    parser.add_argument('--num-classes', type=int, default=1)

    # SageMakerの動作に関する引数
    # モデルの出力先パスを指定
    parser.add_argument('--model-dir', type=str, default=os.environ.get('SM_MODEL_DIR'))
    # 学習データの入力パスを指定
    parser.add_argument('--train', type=str, default=os.environ.get('SM_CHANNEL_TRAIN'))
    parser.add_argument('--validation', type=str, default=os.environ.get('SM_CHANNEL_VALIDATION'))

    args, _ = parser.parse_known_args()
    
    # データをPandasのDataFrameとして取得
    # ディレクトリに複数ファイルある場合にも対応
    train_inputs = [ os.path.join(args.train, file) for file in os.listdir(args.train) ]
    train_raw_data = [ pd.read_csv(file, header=None, engine="python") for file in train_inputs ]
    train_data = pd.concat(train_raw_data)

    # 読み込んだデータを学習データと正解ラベルに分ける
    train_y = train_data.ix[:,0]
    train_x = train_data.ix[:,1:]

    valid_inputs = [ os.path.join(args.validation, file) for file in os.listdir(args.validation) ]
    valid_raw_data = [ pd.read_csv(file, header=None, engine="python") for file in valid_inputs ]
    valid_data = pd.concat(valid_raw_data)

    valid_y = valid_data.ix[:,0]
    valid_x = valid_data.ix[:,1:]
    
    
    # ニューラルネットモデルの構築
    model = Sequential()
    model.add(Dense(256, activation='relu', input_shape=(train_x.shape[1],))) #このソース内では tr_x_scale ではない
    model.add(Dropout(0.2))
    model.add(Dense(256, activation='relu'))
    model.add(Dropout(0.2))
    model.add(Dense(1, activation='sigmoid'))

    model.compile(loss='binary_crossentropy',
                  optimizer='adam',
                  metrics=['accuracy'])

    # 学習
    model.fit(train_x, train_y,
              batch_size=args.batch_size,
              epochs=args.epochs,
              verbose=1,
              validation_data=(valid_x, valid_y))
    
    ### Validation
    score = model.evaluate(valid_x, valid_y)
    print('loss : {}'.format(score[0]))
    print('acc : {}'.format(score[1]))
    
    ### モデル保存
    sess = K.get_session()
    tf.saved_model.simple_save(
        sess,
        os.path.join(args.model_dir, 'model/1'),
        inputs={'inputs': model.input},
        outputs={t.name: t for t in model.outputs})
```

# 詳細
あとで

# 参考

Amazon SageMakerのScript modeを使ってKerasを動かしてみた #reinvent
https://dev.classmethod.jp/articles/sagemaker-keras-script-mode/