Amazon SageMaker Model Monitorを試してみた #reinvent

SageMakerの運用管理が、また少し楽になりました。
2019.12.05

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

最初に

こんにちはデータアナリティクス事業本部のyoshimです。
今日はre:Invent2019にて「Amazon SageMaker Model Monitor」のリリースが発表されたので、早速試してみました。

「Amazon SageMaker Model Monitor」は「モデルをリリースして以降にデータの品質が変わってしまったことを検知する」、という実際にプロダクションでの利用を検討している人にとってはとても嬉しい機能だと思います。
私は運用が楽になりそうな機能がとても好きなので、とてもワクワクしています。

目次

1.Amazon SageMaker Model Monitorとは

まず、このサービスが何をしてくれるのかを一言で言うならば「推論時に投入されるデータの質が、モデルを学習した時と変化していないかをチェックしてアラートをあげたりレポートを出力してくれる」サービスです。

機械学習のモデルをデプロイした後に、時間の経過や何らかの外部環境の影響に伴い「推論に投入するデータの変化」があった場合、モデルをそのままにしておくと精度の低下等の問題が発生します。
このような事象に対して「Amazon SageMaker Model Monitor」を使うと楽に管理することができます。
下記は参照先のリンクの文章をそのまま転載しています。

How Model Monitor Works Amazon SageMaker Model Monitor automatically monitors machine learning (ML) model in production and notifies you when data quality issues arise. ML models in production have to make predictions on real-life data that is not carefully curated like most training datasets. If the statistical nature of the data that your model receives while in production drifts away from the nature of the baseline data it was trained on, the model begins to lose accuracy in its predictions. Model Monitor uses rules to detect data drift and alerts you when it happens. The following figure shows how this process works.

参照:Amazon SageMaker Model Monitor

下記は上記のリンクから取得した「Amazon SageMaker Model Monitor」がどのように動いているのか、を解説した画像です。
「Amazon SageMaker Model Monitor」がどのような動作をするのか、もう少し詳細を見てみましょう。

公式ドキュメントでは、処理動作を下記の4点に整理していましたので、下記の4点と上記の画像と対応させて説明します。

Data Capture: Enable the endpoint to capture data from incoming requests to a trained ML model and the resulting model predictions.
Baseline: Create a baseline from the dataset that was used to train the model. Compute baseline schema constraints and statistics for each feature using Deequ, an open source library built on Apache Spark, which is used to measure data quality in large datasets.
Schedule: Create a monitoring schedule specifying what data to collect, how often to collect it, how to analyze it, and which reports to produce.
Interpretation: Inspect the reports, which compare the latest data with the baseline, and watch for any violations reported and for metrics and notifications from Amazon CloudWatch.

まずは、指定したデータセット(学習に利用したデータを使うことが一般的かと思います)から各特徴量の統計量等のベースラインを取得します。
ここで取得したベースラインは、デプロイ後に推論エンドポイントに投入されたデータの特徴量の統計量と比較するために利用されます。

Baseline: Create a baseline from the dataset that was used to train the model. Compute baseline schema constraints and statistics for each feature using Deequ, an open source library built on Apache Spark, which is used to measure data quality in large datasets.

続いて、推論エンドポイントが「データをキャプチャ」することを許可します。
これで、推論エンドポイントは「推論時に投入されたデータ」と「推論結果」の両方を取得し、S3バケットに出力することができます。

Data Capture: Enable the endpoint to capture data from incoming requests to a trained ML model and the resulting model predictions.

続いて、モニタリングのスケジュールを設定します。
モニタリングは「どのようなデータを取得するか」、「モニタリングの頻度」、「分析方法」、「どのようなレポートを出力するか」の4点を設定できます。

Schedule: Create a monitoring schedule specifying what data to collect, how often to collect it, how to analyze it, and which reports to produce.

最後に、レポートの結果を解釈します。
最新の結果とベースラインを比較して、何か異常なところが無いかを確認し、場合によってはモデルの再学習や更新が必要となります。

Interpretation: Inspect the reports, which compare the latest data with the baseline, and watch for any violations reported and for metrics and notifications from Amazon CloudWatch.

「Amazon SageMaker Model Monitor」の概要については以上です。
続いて、実際に操作してみて、より具体的なイメージをつかんでいきます。

2.やってみた

こちらの内容を実際に手を動かして確認してみました。

2-1.用意されているモデルをエンドポイントにデプロイする

まずは、データをキャプチャーする設定を記述した後にAWS側で事前に用意してくれているモデルを推論エンドポイントとしてデプロイします。

from sagemaker.model_monitor import DataCaptureConfig

endpoint_name = 'DEMO-xgb-churn-pred-model-monitor-' + strftime("%Y-%m-%d-%H-%M-%S", gmtime())
print("EndpointName={}".format(endpoint_name))

# データキャプチャーの設定
data_capture_config = DataCaptureConfig(
                        enable_capture=True,
                        sampling_percentage=100, # どれだけの割合のデータをキャプチャーするか
                        destination_s3_uri=s3_capture_upload_path) # キャプチャーした結果をどこに出力するか

predictor = model.deploy(initial_instance_count=1,
                instance_type='ml.m4.xlarge',
                endpoint_name=endpoint_name,
                data_capture_config=data_capture_config)

この時点では、まだキャプチャーの設定をしただけなのでなにもファイルは出力されていません。

!aws s3 ls s3://<my-bucket>/sagemaker/DEMO-ModelMonitor/datacapture
!aws s3 ls s3://<my-bucket>/sagemaker/DEMO-ModelMonitor/reports

2-2.適当に推論してみて、どんなデータが出力されるかを確認する

推論エンドポイントに適当なデータを推論として投げて、ちゃんとデータがキャプチャーされてS3に出力されているかを確認します。 ここでは推論の中身にはあまり意味はありません。

from sagemaker.predictor import RealTimePredictor
import time

predictor = RealTimePredictor(endpoint=endpoint_name,content_type='text/csv')

# get a subset of test data for a quick test
!head -120 test_data/test-dataset-input-cols.csv > test_data/test_sample.csv
print("Sending test traffic to the endpoint {}. \nPlease wait...".format(endpoint_name))

with open('test_data/test_sample.csv', 'r') as f:
    for row in f:
        payload = row.rstrip('\n')
        response = predictor.predict(data=payload)
        time.sleep(0.5)
        
print("Done!")

キャプチャー設定がちゃんと反映されて、ファイルが出力されていました。
また、このファイル出力のパス設定としては、下記のようになっているようです。

s3://{destination-bucket-prefix}/{endpoint-name}/{variant-name}/yyyy/mm/dd/hh/filename.jsonl

「時間」単位で異なるパス設定がされ、また「hh」配下のファイルも1ファイルにまとまるわけではなく、ある程度の時間間隔ごとに異なるファイルが出力されているように見えます。

# fileチェック
s3_client = boto3.Session().client('s3')
current_endpoint_capture_prefix = '{}/{}'.format(data_capture_prefix, endpoint_name)
result = s3_client.list_objects(Bucket=bucket, Prefix=current_endpoint_capture_prefix)
capture_files = [capture_file.get("Key") for capture_file in result.get('Contents')]
print("Found Capture Files:")
print("\n ".join(capture_files))

ファイルの中身を少し確認してみると下記のような形でした。
(元はjsonlでしたが、jsonの方がみやすかったのでフォーマットを変換しています)

推論ごとに「Input」、「Output」、「Metadata」を保持しています。

{
	"captureData": {
		"endpointInput": {
			"observedContentType": "text/csv",
			"mode": "INPUT",
			"data": "92,0,176.3,85,93.4,125,207.2,107,9.6,1,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,1,0",
			"encoding": "CSV"
		},
		"endpointOutput": {
			"observedContentType": "text/csv; charset=utf-8",
			"mode": "OUTPUT",
			"data": "0.039806101471185684",
			"encoding": "CSV"
		}
	},
	"eventMetadata": {
		"eventId": "b63392a0-2686-4f94-95b5-dc3c6fe5ac09",
		"inferenceTime": "2019-12-04T17:17:19Z"
	},
	"eventVersion": "0"
} {
	"captureData": {
		"endpointInput": {
			"observedContentType": "text/csv",
			"mode": "INPUT",
			"data": "138,0,46.5,104,186.0,114,167.5,95,9.6,4,4,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,1,0",
			"encoding": "CSV"
		},
		"endpointOutput": {
			"observedContentType": "text/csv; charset=utf-8",
			"mode": "OUTPUT",
			"data": "0.9562002420425415",
			"encoding": "CSV"
		}
	},
	"eventMetadata": {
		"eventId": "eb5cd8a1-2268-4045-9f21-31f7f9b9e833",
		"inferenceTime": "2019-12-04T17:17:20Z"
	},
	"eventVersion": "0"
} {
	"captureData": {
		"endpointInput": {
			"observedContentType": "text/csv",
			"mode": "INPUT",
			"data": "93,0,176.1,103,199.7,130,263.9,96,8.5,6,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1,0,1,0,1,0",
			"encoding": "CSV"
		},
		"endpointOutput": {
			"observedContentType": "text/csv; charset=utf-8",
			"mode": "OUTPUT",
			"data": "0.007474285550415516",
			"encoding": "CSV"
		}
	},
	"eventMetadata": {
		"eventId": "f0f0fce5-655a-4ada-b4f6-91becb8dec9a",
		"inferenceTime": "2019-12-04T17:17:21Z"
	},
	"eventVersion": "0"
}

2-3.ベースラインを作成

ここまででは、まだ「推論されたデータのInput、OutputをS3に保存する」といったことまでしかできていません。
逆に言うと、それだけで十分、といったユースケースなのならここまででも十分かと思います。

しかし、今回は「推論したデータが学習に利用したデータと異なるか、といったレポートの作成」までやりたいので、推論で投入されるデータと比較するための「ベースライン」を作成します。
これは「モデルの学習に利用したデータ」を元にベースラインを作成することが殆どかと思いますが、一応異なるデータも指定できるようです。

from sagemaker.model_monitor import DefaultModelMonitor
from sagemaker.model_monitor.dataset_format import DatasetFormat

my_default_monitor = DefaultModelMonitor(
    role=role,
    instance_count=1,
    instance_type='ml.m5.xlarge',
    volume_size_in_gb=20,
    max_runtime_in_seconds=3600,
)

my_default_monitor.suggest_baseline(
    baseline_dataset=baseline_data_uri+'/training-dataset-with-header.csv',
    dataset_format=DatasetFormat.csv(header=True),
    output_s3_uri=baseline_results_uri,
    wait=True
)

さて、続いて上記で作成したベースラインを使ってモニタリングするスケジュールを登録してみましょう。

2-4.モニタリングのスケジュールを作成

モニタリングのスケジュールを作成する際に、色々なオプションを指定できます。
例えば、「いつ実行するか」、「どのような統計情報を取得するか」等です。
とりあえずやる、といった形なら下記のようにあらかじめ用意されている設定を利用することもできます。

from sagemaker.model_monitor import CronExpressionGenerator
from time import gmtime, strftime

mon_schedule_name = 'DEMO-xgb-churn-pred-model-monitor-schedule-' + strftime("%Y-%m-%d-%H-%M-%S", gmtime())
my_default_monitor.create_monitoring_schedule(
    monitor_schedule_name=mon_schedule_name, # モニタリングスケジュールの名前。AWSアカウント、リージョンごとに一意である必要がある
    endpoint_input=predictor.endpoint, # モニタリング対象のエンドポイント
    post_analytics_processor_script=s3_code_postprocessor_uri, # 後処理するスクリプトをS3に事前に用意しておいて、そのパスを指定する。
    output_s3_uri=s3_report_path, # モニタリング結果を出力するS3パス
    statistics=my_default_monitor.baseline_statistics(),
    constraints=my_default_monitor.suggested_constraints(),
    schedule_cron_expression=CronExpressionGenerator.hourly(),
    enable_cloudwatch_metrics=True,
)

参考:create_monitoring_schedule

これでモニタリングのスケジュールが登録できました。
この後、適当なデータで推論を実行して、早速モニタリング結果のレポートを確認してみましょう。

2-5.モニタリングして出力したレポートの中身を確認

さて、適当なデータで推論した結果をモニタリングをしたところ、「statistics.json」、「constraints.json」、「constraint_violations.json」の3ファイルが確認できましたので、それぞれ中身を見てみます。
また、S3ファイルパスについては下記のように「時間単位」で出力されていました。
(モニタリングスケジュールを時間単位で指定したため)

s3://{プレフィックス}/{エンドポイント名}/{スケジュール名}/YYYY/MM/DD/HH/'

  • statistics.json

特徴量ごとに、平均や合計、標準偏差等色々な統計量を取得していました。

{
    "name" : "VMail Plan_no",
    "inferred_type" : "Fractional",
    "numerical_statistics" : {
      "common" : {
        "num_present" : 1798,
        "num_missing" : 0
      },
      "mean" : 0.7221357063403783,
      "sum" : 1298.4,
      "std_dev" : 0.45002718919693313,
      "min" : 0.0,
      "max" : 1.4,
      "distribution" : {
        "kll" : {
          "buckets" : [ {
            "lower_bound" : 0.0,
            "upper_bound" : 0.13999999999999999,
            "count" : 502.0
          }, {
            "lower_bound" : 0.13999999999999999,
            "upper_bound" : 0.27999999999999997,
            "count" : 0.0
          }, {
            "lower_bound" : 0.27999999999999997,
            "upper_bound" : 0.41999999999999993,
            "count" : 0.0
          }, {
            "lower_bound" : 0.41999999999999993,
            "upper_bound" : 0.5599999999999999,
            "count" : 0.0
          }, {
            "lower_bound" : 0.5599999999999999,
            "upper_bound" : 0.7,
            "count" : 0.0
          }, {
            "lower_bound" : 0.7,
            "upper_bound" : 0.8399999999999999,
            "count" : 0.0
          }, {
            "lower_bound" : 0.8399999999999999,
            "upper_bound" : 0.9799999999999999,
            "count" : 0.0
          }, {
            "lower_bound" : 0.9799999999999999,
            "upper_bound" : 1.1199999999999999,
            "count" : 1290.0
          }, {
            "lower_bound" : 1.1199999999999999,
            "upper_bound" : 1.26,
            "count" : 0.0
          }, {
            "lower_bound" : 1.26,
            "upper_bound" : 1.4,
            "count" : 6.0
          } ],
          "sketch" : {
            "parameters" : {
              "c" : 0.64,
              "k" : 2048.0
            },
            "data" : [ [ 0.0, 1.0, 0.0, 0.0, 1.0, ...実際に入っているデータが特徴量の分続く] ]
          }
        }
      }
    }
  },以下、特徴量ごとに同じような内容が続く
  • constraints.json

特徴量ごとにNULLがあるか、とか負の値があるか、とかをチェックしていました。

{
  "version" : 0.0,
  "features" : [ {
    "name" : "Churn",
    "inferred_type" : "Fractional",
    "completeness" : 1.0,
    "num_constraints" : {
      "is_non_negative" : true
    }
  }, {
    "name" : "Area Code_510",
    "inferred_type" : "Fractional",
    "completeness" : 1.0,
    "num_constraints" : {
      "is_non_negative" : true
    }
  }, {
    "name" : "Int'l Plan_no",
    "inferred_type" : "Fractional",
    "completeness" : 1.0,
    "num_constraints" : {
      "is_non_negative" : true
    }
  }, 
    .
    .
    .
    特徴量ごとに続く
    {
    "name" : "VMail Plan_yes",
    "inferred_type" : "Fractional",
    "completeness" : 1.0,
    "num_constraints" : {
      "is_non_negative" : true
    }
  } ],
  "monitoring_config" : {
    "evaluate_constraints" : "Enabled",
    "emit_metrics" : "Enabled",
    "datatype_check_threshold" : 1.0,
    "domain_content_threshold" : 1.0,
    "distribution_constraints" : {
      "perform_comparison" : "Enabled",
      "comparison_threshold" : 0.1,
      "comparison_method" : "Robust"
    }
  }
  • constraint_violations.json

今回は違反を盛り込んだ状態で推論をしているのですが、下記の通り「どのようなところで、どのような違反があったのか」といった点が確認できました。
これは違反内容がわかりやすくていいですね。

{
  "violations" : [ {
    "feature_name" : "State_MN",
    "constraint_check_type" : "data_type_check",
    "description" : "Data type match requirement is not met. Expected data type: Integral, Expected match: 100.0%. Observed: Only 99.6662958843159% of data is Integral."
  }, {
    "feature_name" : "State_KY",
    "constraint_check_type" : "data_type_check",
    "description" : "Data type match requirement is not met. Expected data type: Integral, Expected match: 100.0%. Observed: Only 99.6662958843159% of data is Integral."
  }, {
    "feature_name" : "VMail Plan_yes",
    "constraint_check_type" : "data_type_check",
    "description" : "Data type match requirement is not met. Expected data type: Integral, Expected match: 100.0%. Observed: Only 99.6662958843159% of data is Integral."
  }, {
    "feature_name" : "State_NC",
    "constraint_check_type" : "data_type_check",
    "description" : "Data type match requirement is not met. Expected data type: Integral, Expected match: 100.0%. Observed: Only 99.6662958843159% of data is Integral."
   } ]

3.まとめ

「Amazon SageMaker Model Monitor」を一通り触ってみました。
ここではまだできていないのですが、違反があった場合にアラートをあげる設定も早く試してみたいですね。
SageMakerをプロダクションで実運用している方には是非オススメの機能です。

4.参考