「一切有可能产生错误的事情通常都会出错」,墨菲定律的这个「乌鸦嘴魔咒」似乎是不可能避免的。也许每次都是面包上涂了果酱的那一面与地板亲密接触,也许是平时公司楼下总有等客的出租车而当你着急外出时却一辆也看不见,或者你精心制作的 ML 模型经过多次训练和调教最终不可避免崩了……
「一切有可能产生错误的事情通常都会出错」,当你这样安慰自己的时候,难道就不好奇,为什么偏偏就会这样?
构建和训练 ML 模型是科学和工艺的结合。从收集和准备数据集,到使用不同算法进行实验以找出最佳训练参数(可怕的超参数),ML 从业者需要清除很多障碍才能提供高性能模型。这正是亚马逊云科技 (Amazon Web Services)提供 Amazon SageMaker 服务的原因之一,就是为了帮助大家简化和加快 ML 工作流的构建过程。
然而估计很多人都深有体会,ML 似乎是墨菲定律最喜欢的地方之一:一切有可能产生错误的事情通常都会出错!特别是,训练过程中可能发生很多模糊的问题,导致模型无法正确提取或难以学习数据集中的模型。但原因通常并不在于 ML 库中的软件漏洞(尽管它们也会发生),大多数失败的训练作业都是因为不适当的参数初始化、糟糕的超参数组合、我们自己代码中的设计问题等造成的。
更糟的是,这些问题很少能立即显现出来,它们往往会随着时间的推移放大,从而慢慢但也必定会破坏我们的训练过程,产生准确度低的模型。面对现实吧,即使高水平的专家大牛,识别并揪出这些问题也非常困难且耗时。
Amazon SageMaker Debugger 成为解救者
最近,Amazon SageMaker Debugger 正式发布,它是 Amazon SageMaker 的新功能,可以自动识别机器学习(ML)训练作业中出现的复杂问题。
在我们现有的 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 调试 ML 模型
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 的所有商业区域推出,欢迎体验。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。