Amazon Web Services ブログ

Amazon SageMaker Debugger – 機械学習モデルのデバッガ

2019年12月3日、機械学習(ML)学習時に起こる複雑な問題を自動的に識別する Amazon SageMaker の新しい機能、Amazon SageMaker Debugger を発表できて非常にうれしく思います。

機械学習モデルの構築と学習は、サイエンスと工芸の融合です(魔術と言う人もいます)。データセットの収集から準備、さまざまなアルゴリズムの実験、最適なトレーニングパラメーター(恐ろしいハイパーパラメーター)の探索まで、機械学習を実行する人は高性能のモデルを提供するために多くのハードルをクリアする必要があります。これがまさに、機械学習ワークフローを簡素化し高速化する、モジュール式のフルマネージドサービス Amazon SageMaker を構築する理由なのです。

調べていると、機械学習はマーフィーのお気に入りのたまり場であるように思われます。特に学習プロセス中には、多くの不明瞭な問題により、データセットに存在するパターンの抽出や学習が正しくできない可能性があります。私がここでお話ししたいのは、機械学習ライブラリのソフトウェアバグではなく(実際に発生しますが)、学習ジョブの失敗のほとんどは、パラメーターの不適切な初期化、ハイパーパラメーターの不適切な組み合わせ、独自のコードの設計問題などが原因です。

さらによろしくないことに、これらの問題がすぐに目に見えてくることは滅多にありません。時間とともに成長し、ゆっくりですが確実にトレーニングプロセスを台無しにし、精度の低いモデルを生み出してしまいます。あなたが本物の専門家であっても、その問題に直面すると、それらを特定し、追い詰めるのは非常に困難で時間がかかるものです。Amazon SageMaker Debugger は、その課題解決のために生まれました。

Amazon SageMaker Debugger のご紹介

TensorFlow、Keras、Apache MXNet、PyTorch、XGBoost など、既存のトレーニングコードでは、新しい SageMaker Debugger SDK を使用して、定期的な間隔で内部モデルの状態を保存できます。ご想像のとおり、それらは Amazon Simple Storage Service(S3) に保存されます。

内部モデルの状態は次のようなもので構成されます。

  • モデルによって学習されているパラメーター、たとえばニューラルネットワークの重みとバイアス
  • オプティマイザーによってこれらのパラメーターに適用される変更、別名勾配
  • 最適化パラメーター自体
  • 精度や損失などのスカラー値
  • 各レイヤーの出力
  • などなど

特定の値の各セット(たとえば、時間の経過とともに特定のニューラルネットワーク層を流れる勾配のシーケンス)はテンソルと呼ばれ、個別に保存されます。テンソルはコレクション(重み、勾配など)に応じて整理されており、学習中にどれを保存したいか決定することができます。次に、SageMaker SDK と Estimator を使用して、通常どおりトレーニングジョブを構成し、SageMaker Debugger に適用するルールを定義する追加のパラメーターを渡します。

ルールは Python コードで書かれ、学習中のモデルのテンソルを分析し、特定の不要な条件を探します。テンソルの急激な増加や消滅(NaNまたはゼロ値に達するパラメーター)、勾配の爆発や消滅、変化しない損失などの一般的な問題に対して、事前定義されたルールを使用できます。もちろん、独自のルールを作成することもできます。

ひとたび SageMaker の Estimator が設定されると、学習ジョブを起動できます。直ちに、構成したルールごとにデバッグジョブが起動され、利用可能なテンソルの検査が開始されます。デバッグジョブが問題を検知すると、デバッグジョブは停止し、追加情報を記録します。CloudWatch Events というイベントも送信され、自動化された追加ステップをトリガする必要も出てきます。

ディープラーニングを扱う際に、勾配の消失などに苦しんでいることがわかりましたね。少しブレーンストーミングと経験を積むと、どこに着目すべきかがわかってきます。ニューラルネットワークの階層が深すぎるのでしょうか、それとも学習率が小さすぎるのでしょうか。内部状態が S3 に保存されたので、SageMaker Debugger SDK を使用して、時間の経過に伴うテンソルの進化を調べ、仮説を確認、根本原因を修正することができるようになりました。

SageMaker Debugger の動作を簡単なデモで見てみましょう。

Amazon SageMaker Debugger による機械学習モデルのデバッグ

SageMaker デバッガの中核は、トレーニング中にテンソルをキャプチャする機能です。これを実行するには、保存したいテンソルコレクションと保存する頻度、値自体を直接保存するか平均などリダクションを保存するかなどをの選択するため、トレーニングコードの計装が少し必要です。

この目的のために、SageMaker Debugger SDK は、サポートする各フレームワークごとにシンプルな API を提供します。 TensorFlow を使った2次元の線形回帰モデルのシンプルなスクリプトを使って、この機能がどのように機能するかを見てみましょう。もちろん、このGithub リポジトリにはさらに多くの例があります。

まず初期コードを見てみましょう。

import argparse
import numpy as np
import tensorflow as tf
import random

parser = argparse.ArgumentParser()
parser.add_argument('--model_dir', type=str, help="S3 path for the model")
parser.add_argument('--lr', type=float, help="Learning Rate", default=0.001)
parser.add_argument('--steps', type=int, help="Number of steps to run", default=100)
parser.add_argument('--scale', type=float, help="Scaling factor for inputs", default=1.0)

args = parser.parse_args()

with tf.name_scope('initialize'):
    # 2-dimensional input sample
    x = tf.placeholder(shape=(None, 2), dtype=tf.float32)
    # Initial weights: [10, 10]
    w = tf.Variable(initial_value=[[10.], [10.]], name='weight1')
    # True weights, i.e. the ones we're trying to learn
    w0 = [[1], [1.]]
with tf.name_scope('multiply'):
    # Compute true label
    y = tf.matmul(x, w0)
    # Compute "predicted" label
    y_hat = tf.matmul(x, w)
with tf.name_scope('loss'):
    # Compute loss
    loss = tf.reduce_mean((y_hat - y) ** 2, name="loss")

optimizer = tf.train.AdamOptimizer(args.lr)
optimizer_op = optimizer.minimize(loss)

with tf.Session() as sess:
    sess.run(tf.global_variables_initializer())
    for i in range(args.steps):
        x_ = np.random.random((10, 2)) * args.scale
        _loss, opt = sess.run([loss, optimizer_op], {x: x_})
        print (f'Step={i}, Loss={_loss}')

TensorFlow Estimator を使用してこのスクリプトを学習しましょう。ここでは、SageMaker ローカルモードを使用しています。ローカルモードは実験的なコードをすばやく反復検証するのに最適な方法です。

bad_hyperparameters = {'steps': 10, 'lr': 100, 'scale': 100000000000}

estimator = TensorFlow(
role=sagemaker.get_execution_role(),
base_job_name='debugger-simple-demo',
train_instance_count=1,
train_instance_type='local',
entry_point='script-v1.py',
framework_version='1.13.1',
py_version='py3',
script_mode=True,
hyperparameters=bad_hyperparameters)

トレーニングログを見ると、うまくいかなかったようです。

Step=0, Loss=7.883463958023267e+23
algo-1-hrvqg_1 | Step=1, Loss=9.502028841062608e+23
algo-1-hrvqg_1 | Step=2, Loss=nan
algo-1-hrvqg_1 | Step=3, Loss=nan
algo-1-hrvqg_1 | Step=4, Loss=nan
algo-1-hrvqg_1 | Step=5, Loss=nan
algo-1-hrvqg_1 | Step=6, Loss=nan
algo-1-hrvqg_1 | Step=7, Loss=nan
algo-1-hrvqg_1 | Step=8, Loss=nan
algo-1-hrvqg_1 | Step=9, Loss=nan

損失は​​まったく減少せず、無限大に発散してしまっています。これはテンソルの爆発問題で、 SageMaker Debugger で定義されている組み込みルールの1つです。さぁ、仕事に取り掛かりましょう。

Amazon SageMaker Debugger SDK の使用

テンソルをキャプチャするために、トレーニングスクリプトに以下を装備する必要があります。

  • SaveConfig: テンソルを保存する頻度を指定するオブジェクト
  • SessionHookTensorFlow: セッションに接続され、トレーニング中にすべてをまとめて必要なテンソルを保存するオブジェクト
  • (オプション) ReductionConfig :フルテンソルの代わりに保存する必要があるテンソル縮約をリストするオブジェクト
  • (オプション) 勾配をキャプチャするオプティマイザーラッパー

以下が、SageMaker Debugger のパラメータ用にコマンドライン引数を含む更新されたコードです。

import argparse
import numpy as np
import tensorflow as tf
import random
import smdebug.tensorflow as smd

parser = argparse.ArgumentParser()
parser.add_argument('--model_dir', type=str, help="S3 path for the model")
parser.add_argument('--lr', type=float, help="Learning Rate", default=0.001 )
parser.add_argument('--steps', type=int, help="Number of steps to run", default=100 )
parser.add_argument('--scale', type=float, help="Scaling factor for inputs", default=1.0 )
parser.add_argument('--debug_path', type=str, default='/opt/ml/output/tensors')
parser.add_argument('--debug_frequency', type=int, help="How often to save tensor data", default=10)
feature_parser = parser.add_mutually_exclusive_group(required=False)
feature_parser.add_argument('--reductions', dest='reductions', action='store_true', help="save reductions of tensors instead of saving full tensors")
feature_parser.add_argument('--no_reductions', dest='reductions', action='store_false', help="save full tensors")
args = parser.parse_args()
args = parser.parse_args()

reduc = smd.ReductionConfig(reductions=['mean'], abs_reductions=['max'], norms=['l1']) if args.reductions else None

hook = smd.SessionHook(out_dir=args.debug_path,
include_collections=['weights', 'gradients', 'losses'],
save_config=smd.SaveConfig(save_interval=args.debug_frequency),
reduction_config=reduc)

with tf.name_scope('initialize'):
# 2-dimensional input sample
x = tf.placeholder(shape=(None, 2), dtype=tf.float32)
# Initial weights: [10, 10]
w = tf.Variable(initial_value=[[10.], [10.]], name='weight1')
# True weights, i.e. the ones we're trying to learn
w0 = [[1], [1.]]
with tf.name_scope('multiply'):
# Compute true label
y = tf.matmul(x, w0)
# Compute "predicted" label
y_hat = tf.matmul(x, w)
with tf.name_scope('loss'):
# Compute loss
loss = tf.reduce_mean((y_hat - y) ** 2, name="loss")
hook.add_to_collection('losses', loss)

optimizer = tf.train.AdamOptimizer(args.lr)
optimizer = hook.wrap_optimizer(optimizer)
optimizer_op = optimizer.minimize(loss)

hook.set_mode(smd.modes.TRAIN)

with tf.train.MonitoredSession(hooks=[hook]) as sess:
for i in range(args.steps):
x_ = np.random.random((10, 2)) * args.scale
_loss, opt = sess.run([loss, optimizer_op], {x: x_})
print (f'Step={i}, Loss={_loss}')

また、TensorFlow を変更して Estimator、SageMaker Debugger 対応のトレーニングコンテナを使用し、追加のパラメータを渡す必要があります。

bad_hyperparameters = {'steps': 10, 'lr': 100, 'scale': 100000000000, 'debug_frequency': 1}

from sagemaker.debugger import Rule, rule_configs
estimator = TensorFlow(
role=sagemaker.get_execution_role(),
base_job_name='debugger-simple-demo',
train_instance_count=1,
train_instance_type='ml.c5.2xlarge',
image_name=cpu_docker_image_name,
entry_point='script-v2.py',
framework_version='1.15',
py_version='py3',
script_mode=True,
hyperparameters=bad_hyperparameters,
rules = [Rule.sagemaker(rule_configs.exploding_tensor())]
)

estimator.fit()
2019-11-27 10:42:02 Starting - Starting the training job...
2019-11-27 10:42:25 Starting - Launching requested ML instances
********* Debugger Rule Status *********
*
* ExplodingTensor: InProgress
*
****************************************

ここでは、実際のトレーニングジョブと、Estimator で定義されたルールをチェックするデバッグジョブの2つのジョブが実行されています。すぐに、デバッグジョブは失敗します!

学習ジョブの description を実行すると、何が起こったのか詳細な情報を得ることができます。

description = client.describe_training_job(TrainingJobName=job_name)
print(description['DebugRuleEvaluationStatuses'][0]['RuleConfigurationName'])
print(description['DebugRuleEvaluationStatuses'][0]['RuleEvaluationStatus'])

ExplodingTensor
IssuesFound

保存されたテンソルを見てみましょう。

テンソルの探索

トレーニングプロセス中に S3 に保存されたテンソルを簡単に取得できます。

s3_output_path = description["DebugConfig"]["DebugHookConfig"]["S3OutputPath"]
trial = create_trial(s3_output_path)

利用可能なテンソルをリストしましょう。

trial.tensors()

['loss/loss:0', 'gradients/multiply/MatMul_1_grad/tuple/control_dependency_1:0', 'initialize/weight1:0']

すべての値は numpy 配列であり、簡単に繰り返し処理できます。

tensor = 'gradients/multiply/MatMul_1_grad/tuple/control_dependency_1:0'
for s in list(trial.tensor(tensor).steps()):
print("Value: ", trial.tensor(tensor).step(s).value)

Value: [[1.1508383e+23] [1.0809098e+23]]
Value: [[1.0278440e+23] [1.1347468e+23]]
Value: [[nan] [nan]]
Value: [[nan] [nan]]
Value: [[nan] [nan]]
Value: [[nan] [nan]]
Value: [[nan] [nan]]
Value: [[nan] [nan]]
Value: [[nan] [nan]]
Value: [[nan] [nan]]

テンソル名にはトレーニングコードで定義された TensorFlow スコープが含まれているため、行列乗算に問題があることが簡単にわかります。

# Compute true label
y = tf.matmul(x, w0)
# Compute "predicted" label
y_hat = tf.matmul(x, w)

少し掘り下げてみます、x 入力はスケーリングパラメーターによって変更されますが、この値は、Estimator で 100000000000 と設定しました。学習率も正気に見えません。ビンゴ!

x_ = np.random.random((10, 2)) * args.scale

bad_hyperparameters = {'steps': 10, 'lr': 100, 'scale': 100000000000, 'debug_frequency': 1}

もうおわかりだと思いますが、これらのハイパーパメーターをより合理的な値に設定することで、この学習の問題が修正されます。

今すぐ利用可能です!

Amazon SageMaker Debugger を使って、学習の問題をより迅速に発見して解決できると信じています。さぁ、あなたのバグハンティングの番です。

このサービスは、Amazon SageMaker が 利用可能なすべてのAWS リージョンで現在利用可能です。お試しいただき、Amazon SageMaker の AWS フォーラム、または通常の AWS へのご連絡先からフィードバックをお寄せください。

− Julian

翻訳は Machine Learning SA の宇都宮が担当しました。原文は、こちら