SageMaker Debuggerを使ってみた（XGBoost編）

[ABEJA Advent Calendar 2019](https://qiita.com/advent-calendar/2019/abeja)の15日目です

## 概要

こんにちは，ABEJAでデータサイエンティストをしているnakamichiです．といっても実はまだ転職して来て間もないヒヨッコなので，特に仕事とは関係なく気になっているデータサイエンスの話題を取り上げたいと思います．

先日の[AWS re:Invent](https://reinvent.awsevents.com/)では，[SageMaker](https://aws.amazon.com/jp/blogs/news/amazon-sagemaker-studio-the-first-fully-integrated-development-environment-for-machine-learning/)をはじめ，SageMaker関係の新機能がいくつか発表されました．本記事ではその中の[SageMaker Debugger](https://github.com/awslabs/sagemaker-debugger)を取り上げます．

SageMaker Debuggerを使うと，深層学習モデルのトレーニング中に重みのテンソルを保存したり，勾配消失のような望ましくないイベントが生じていないかを監視したりできるので，デバッグがはかどる......というのが概略的な説明なのですが，このDebugger，実は深層学習フレームワークだけでなくXGBoostに対しても使えるようです．XGBoostモデルのデバッグでは一体何ができるのか気になります．というわけで，公式に用意されているサンプルの[ノートブック](https://github.com/awslabs/amazon-sagemaker-examples/blob/master/sagemaker-debugger/xgboost_builtin_rules/xgboost-regression-debugger-rules.ipynb)（Debugging XGBoost Training Jobs with Amazon SageMaker Debugger Using Rules）を実際に動かして機能を確認してみます．

※以下，コードはすべてSageMakerのノートブックインスタンスで実行しています．


## XGBoostでSageMaker Debuggerを使う

深層学習フレームワークにおけるSageMaker Debuggerの主な機能は，トレーニング中にテンソルを保存してそれを解析することですが，XGBoostは深層学習ではありません．それではXGBoostに対してDebuggerが何をしてくれるのかというと，テンソルではなく各種のモデル評価指標や変数重要度を追跡することができます．

まずは諸々の準備のためのおまじないを実行します．

```bash
# SageMaker Debuggerのインストール
!python -m pip install smdebug
```

```python
import boto3
import sagemaker
from sagemaker.amazon.amazon_estimator import get_image_uri
# 実行中のnotebookのリージョンを取得
region = boto3.Session().region_name
# XGBoostでDebuggerが使えるバージョンのコンテナイメージを取得
container = get_image_uri(region, "xgboost", repo_version="0.90-2")
```

学習に用いるデータをS3にアップロードします．データはUCI Repositoryの[abaloneデータセット](https://archive.ics.uci.edu/ml/datasets/abalone)で，アワビ（abalone）の年齢を色々な物理的測定値から予測するというタスクです．

```python
from data_utils import load_abalone, upload_to_s3
train_file, validation_file = load_abalone()

bucket = sagemaker.Session().default_bucket()
prefix = "DEMO-smdebug-xgboost-abalone"
upload_to_s3(train_file, bucket, f"{prefix}/train/abalone.train.libsvm")
upload_to_s3(validation_file, bucket, f"{prefix}/validation/abalone.validation.libsvm")
```

さて，ここからがモデルとデバッガの設定になります．トレーニング用のコンテナやハイパーパラメータをを指定して`Estimator`を定義するのは通常のSageMakerの使い方と同様ですが，SageMaker Debuggerを用いたデバッグのための設定が`Estimator`の定義に加わります．Debuggerには，監視すべき評価指標等を指定するフックと，トレーニング中に特定の条件に合致したイベントを検出するためのルール（`Rule`）と呼ばれるオブジェクトを設定します．

```python
from sagemaker import get_execution_role
from sagemaker.estimator import Estimator
from sagemaker.debugger import rule_configs, Rule, DebuggerHookConfig, CollectionConfig

base_job_name = "demo-smdebug-xgboost-regression"
role = get_execution_role()
bucket_path = 's3://{}'.format(bucket)

# フックの設定
save_interval = 5 # 評価指標等の保存間隔
debugger_hook_config = DebuggerHookConfig(
    # 評価指標等の保存先
    s3_output_path=bucket_path,
    # 収集する対象を指定．ここでは評価指標，変数重要度，SHAP値を指定．
    collection_configs=[
        CollectionConfig(name="metrics",
            parameters={"save_interval": str(save_interval)}),
        CollectionConfig(name="feature_importance",
            parameters={"save_interval": str(save_interval)}),
        CollectionConfig(name="average_shap",
            parameters={"save_interval": str(save_interval)}),
    ],
)

# ルールの設定
rule = Rule.sagemaker(
    # ロスが減少しなくなったことを検出する設定
    rule_configs.loss_not_decreasing(),
    rule_parameters={
        "collection_names": "metrics",
        "num_steps": str(save_interval * 2)
    },
)

# モデルの設定
## ハイパーパラメータ
hyperparameters={
    "max_depth": "5",
    "eta": "0.2",
    "gamma": "4",
    "min_child_weight": "6",
    "subsample": "0.7",
    "silent": "0",
    "objective": "reg:squarederror",
    "num_round": "51",
}

estimator = Estimator(
    role=role,
    base_job_name=base_job_name,
    train_instance_count=1,
    train_instance_type='ml.m5.xlarge',
    image_name=container,
    hyperparameters=hyperparameters,
    train_max_run=1800,
    # フックとルールの設定を渡す
    debugger_hook_config=debugger_hook_config,
    rules=[rule]
)
```

モデルとデバッガの設定が済んだので，トレーニングジョブを実行します．

```python
from sagemaker.session import s3_input

train_s3_input = s3_input("s3://{}/{}/{}".format(bucket, prefix, "train"), content_type="libsvm")
validation_s3_input = s3_input( "s3://{}/{}/{}".format(bucket, prefix, "validation"), content_type="libsvm")
estimator.fit({"train": train_s3_input, "validation": validation_s3_input},
    wait=False # ジョブをバックグラウンドで実行
)
```

下記のようにトレーニングやルールの実行状況をリアルタイムに監視することができます．

```python
import time

for _ in range(360):
    job_name = estimator.latest_training_job.name
    client = estimator.sagemaker_session.sagemaker_client
    description = client.describe_training_job(TrainingJobName=job_name)
    training_job_status = description["TrainingJobStatus"]
    rule_job_summary = estimator.latest_training_job.rule_job_summary()
    rule_evaluation_status = rule_job_summary[0]["RuleEvaluationStatus"]
    print(f"Training job status: {training_job_status}, Rule Evaluation Status: {rule_evaluation_status}")

    if rule_evaluation_status in ["Stopped", "IssuesFound", "NoIssuesFound"]:
        break

    time.sleep(10)
```

```
Training job status: InProgress, Rule Evaluation Status: InProgress
Training job status: InProgress, Rule Evaluation Status: InProgress
Training job status: Completed, Rule Evaluation Status: InProgress
...(中略)...
Training job status: Completed, Rule Evaluation Status: IssuesFound
```

実行結果の要約を見るための関数も用意されています．

```python
estimator.latest_training_job.rule_job_summary()
```

```
[{'RuleConfigurationName': 'LossNotDecreasing',
  'RuleEvaluationJobArn': 'arn:aws:sagemaker:us-east-2:************:processing-job/demo-smdebug-xgboost-regre-lossnotdecreasing-********',
  'RuleEvaluationStatus': 'IssuesFound',
  'StatusDetails': 'RuleEvaluationConditionMet: Evaluation of the rule LossNotDecreasing at step 30 resulted in the condition being met\n',
  'LastModifiedTime': datetime.datetime(2019, 12, 16, 5, 26, 22, 732000, tzinfo=tzlocal())}]
```

トレーニングの30ステップ目でロスが減らなくなったことがわかります．ちなみに，ルールに合致するイベントが起きなかった場合は，`'RuleEvaluationStatus': 'NoIssuesFound'`となります．


次は保存しておいた評価指標等の分析を行います．分析を行うために`Trial`オブジェクトを作成します．

```python
from smdebug.trials import create_trial

s3_output_path = estimator.latest_job_debugger_artifacts_path()
trial = create_trial(s3_output_path)
```

保存された評価指標等の一覧を見るには`trial.tensor_names()`メソッドを使います．メソッド名がtensorなのは主に深層学習フレームワークが想定されているからなので，あまり気にせずに．

```
trial.tensor_names()
```

```
['average_shap/f0',
  ...(中略)...
 'average_shap/f8',
 'feature_importance/cover/f1',
  ...(中略)...
 'feature_importance/cover/f8',
 'feature_importance/gain/f1',
  ...(中略)...
 'feature_importance/gain/f8',
 'feature_importance/total_cover/f1',
  ...(中略)...
 'feature_importance/total_cover/f8',
 'feature_importance/total_gain/f1',
  ...(中略)...
 'feature_importance/total_gain/f8',
 'feature_importance/weight/f1',
  ...(中略)...
 'feature_importance/weight/f8',
 'train-rmse',
 'validation-rmse']
```

ここで表示されている名前を`trial.tensor()`メソッドに渡せば，実際に保存されている評価指標等を`Tensor`オブジェクトとして取得できます．

```python
# トレーニングの10ステップ目におけるRMSEの値を取得
trial.tensor("train-rmse").value(10)
```

```
array([2.180568])
```

```python
# 何ステップ目の値が保存されているかは`steps()`で確認可能
trial.tensor("train-rmse").steps()
```

```
[0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50]
```

あとは，これらの値を自由に用いて実験結果の解析とデバッグを行えばよいです．

モデルの評価指標や変数重要度が取得できてもそれほど新味はないかもしれませんが，モデル解釈手法のひとつである[SHAP値](https://github.com/slundberg/shap)を追跡できるのは，SageMaker Debuggerのひとつの特徴でしょう．最後にトレーニング中のSHAP値の変化を見てみます．

```python
import matplotlib.pyplot as plt
import seaborn as sns
import re

def get_data(trial, tname):
    tensor = trial.tensor(tname)
    steps = tensor.steps()
    vals = [tensor.value(s) for s in steps]
    return steps, vals

def plot_collection(trial, collection_name, regex='.*', figsize=(8, 6)):
    fig, ax = plt.subplots(figsize=figsize)
    sns.despine()

    tensors = trial.collection(collection_name).tensor_names

    for tensor_name in sorted(tensors):
        if re.match(regex, tensor_name):
            steps, data = get_data(trial, tensor_name)
            ax.plot(steps, data, label=tensor_name)

    ax.legend(loc='center left', bbox_to_anchor=(1, 0.5))
    ax.set_xlabel('Iteration')

plot_collection(trial, "average_shap")
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58917/cdb5312a-1851-8a0d-0bc2-3da0300e7cce.png)

## まとめと感想

公式のサンプルノートブックに従い，XGBoostでSageMaker Debuggerを試してみました．Debuggerを使うことで，トレーニング中の評価指標，変数重要度，SHAP値等の推移を簡単に保存し解析することができました．本記事では取り上げませんでしたが，モデルそのものや，各データ点における予測値やSHAP値等，[より詳細な情報を追跡することも可能](https://github.com/awslabs/sagemaker-debugger/blob/master/docs/api.md#built-in-collections)です．

もともとXGBoostにせよ深層学習フレームワークにせよ個別のフレームワーク内でコールバック等を利用してデバッグに利用することはできましたが，SageMaker Debuggerではどのフレームワークでも統一的なAPIでフックやルールを書くことができるので，その点はメリットだと思います．フックやルールの便利な設定がデフォルトで色々用意されているのも良いです．

ですが，XGBoostを使うだけであれば，評価指標や変数重要度を見るためだけにわざわざSageMaker Debuggerを使うメリットは少ないかもしれません．ただ，LIMEやSHAPといったモデル解釈のための指標はよく使われるようになってきているものの，トレーニング中もこれらの指標を追いかけて分析まで行っている人はあまりいないのではないでしょうか．現時点ではこれらの指標を簡単に追跡できるのはSageMaker Debuggerだけだと思うので，この機能を使って新たにデバッグや精度改善，モデル解釈のテクニックを考えてみるのはおもしろそうです．