亚马逊AWS官方博客

Amazon SageMaker Debugger – 调试机器学习模型

今天,我们非常高兴地宣布推出支持 Amazon SageMaker Debugger,它是 Amazon SageMaker 的新功能,可以自动识别机器学习 (ML) 训练作业中出现的复杂问题。

构建和训练 ML 模型是科学和工艺的结合(有些人甚至说它是一种巫术)。从收集和准备数据集到使用不同算法进行实验以找出最佳的训练参数(可怕的超参数),ML 从业者需要清除很多障碍才能提供高性能模型。这正是构建 Amazon SageMaker——可简化和加快 ML 工作流的完全托管型模块化服务的原因。

在我不断的观察中,我发现 ML 似乎是墨菲定律最喜欢的地方之一,一切有可能产生错误的事情通常都会出错! 特别是,训练过程中可能发生很多模糊的问题,防止您的模型正确提取和学习您的数据集中的模型。我并不是在谈论 ML 库中的软件漏洞(尽管它们也会发生):大多数失败的训练作业因不适当的参数初始化、糟糕的超参数组合、您自己的代码中的设计问题等造成。

更糟糕的是,这些问题很好能立即显现出来:它们会随着时间的推移放大,从而慢慢但也肯定会破坏您的训练过程,并产生准确度低的模型。让我们面对现实吧,即使您的真正的专家,识别并揪出这些问题也非常困难且耗时,这就是我们构建 Amazon SageMaker Debugger 的原因。

下面我来进行更多介绍。

隆重推出 Amazon SageMaker Debugger

在您现有的 TensorFlow、Keras、Apache MXNet、PyTorch 和 XGBoost 训练代码中,您可以使用新的 SageMaker Debugger 开发工具包定期保存内部模型状态;正如您猜测的那样,它将存储在 Amazon Simple Storage Service (S3) 中。

此状态的组成包括:

  • 模型学习的参数,例如神经网络的权重和偏差,
  • 优化器应用于这些参数的更改,又名梯度,
  • 优化参数本身
  • 标量值,例如准确度和损失,
  • 每一层的输出
  • 等。

每个特定的值集 – 比如,一段时间内在特定神经网络层中流动的梯度顺序独立保存,并且称为张量。张量被组织在集合中(权重、梯度等),您可以决定在训练期间要保存哪些张量。然后,您可以使用 SageMaker 开发工具包及其估算器像往常一样配置您的训练作业,从而传递定义您希望 SageMaker Debugger 应用的规则的其他参数。

规则是一段 Python 代码,可用于分析训练中的模型的张量,以此寻找特定的不需要的条件。预定义规则可用于一些常见问题,如张量爆炸/消失(参数达到 NaN 或零值)、梯度爆炸/消失、损失但未更改等等。当然,您还可以编写自己的规则。

配置 SageMaker 估算器后,您可以启动训练作业。它将为您配置的每个规则立即启动一个调试作业,并开始检查可用的张量。如果调试作业检测到问题,它将停止并记录其他信息。如果您想要触发其他自动化步骤,还会发送 CloudWatch Events 事件。

因此,您现在知道,您的深度学习作业收到所谓的梯度消失影响。只要进行一点头脑风暴,有一点经验,您就会知道去哪里寻找帮助:也许神经网络太深? 也许您的学习速度太低? 由于内部状态已保存到 S3 中,您现在可以使用 SageMaker Debugger 开发工具包探索张量随时间的变化,确认您的假设并修正根本原因。

我们来通过简短的演示了解 SageMaker Debugger 的操作。

使用 Amazon SageMaker Debugger 调试 Machine Learning 模型

SageMaker Debugger 的核心能力是在训练期间获取张量。这需要在您的训练代码中使用一些工具,以选择您想要保存的张量集合,您想要保存它们的频率及您是要保存这些值本身还是缩减值(平均值等)。

为此,SageMaker Debugger 开发工具包为它支持的每个框架提供简单的 API。下面,我来展示它是如何使用简单的 TensorFlow 脚本工作的,以试图拟合一个二维线性回归模型。当然,此 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 中定义的内置规则之一。我们开始吧。

使用 Amazon SageMaker Debugger 开发工具包
为了捕获张量,我需要用以下各项编写训练脚本:

  • 指定应保存张量的频率的 SaveConfig 对象,
  • 附加到 TensorFlow 会话的 SessionHook 对象,将所有部分组合在一起,并在训练期间保存所需的张量,
  • (可选的)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 开始 - 开始训练作业...
2019-11-27 10:42:25 开始 - 启动请求的 ML 实例
********* Debugger Rule Status *********
*
* ExplodingTensor: InProgress 
*
****************************************

两个作业在运行:实际训练作业和检查 Estimator 中定义的规则的调试作业。调试作业很快就失败了!

描述训练作业,我可以得到有关所发生情况的更多信息。

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 输入被缩放参数修改,我在估算器中将该参数设置为 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 Debugger 现已在提供 Amazon SageMaker 的所有商业区域推出。请试一试,并通过 Amazon SageMakerAWS 论坛或您常用的 AWS Support 联系方式向我们发送反馈。

– Julien