SageMaker Model Monitor与Debugger神助攻,理解卷积神经网络这个「黑匣子」

image

很多东西,只有在理解了内部原理和运作机制后,才能更好地改进和增强。然而有些东西,生来就是「不可理解」的,例如卷积神经网络(CNN)。那么该如何进一步完善它?

CNN在图像分类与目标检测等实际任务当中,拥有相当出色的实际表现,也因此被引入多种应用场景当中,包括在自动驾驶领域检测街道上的交通标志与物体、在医疗保健领域准确对基于图像的数据异常进行分类、以及在零售领域进行库存管理等等。

然而,CNN的运作机制如同黑匣子,如果无法理解预测的推理过程,我们很可能在使用时遭遇问题。同样的,在模型部署完成之后,与专门用于模型训练的数据相比,用于推理的数据可能遵循完全不同的数据分布。这种现象通常被称为数据漂移(Data drift),有可能导致模型预测错误。在这种情况下,理解并解释导致模型预测错误的原因就成了冲出迷雾的唯一希望。

类激活映射(Class activation maps)与显著性映射(Saliency maps)等技术方法能够帮助我们直观查看CNN模型如何做出决策。这些图体现为热图形式,能够提示图像素材当中决定预测结果的关键部分。以下示例来自德国交通标志数据集:左侧图像作为经过优化的ResNet模型的输入素材,模型预测结果为该图属于类别25(道路施工)。右图所示为覆盖有热图的输入图像,其中红色表示与类别25预测结论相关度最高的像素,越偏蓝则代表相关度越低。
SageMaker Model Monitor与Debugger神助攻,理解卷积神经网络这个「黑匣子」_第1张图片

如果模型做出错误的预测且无法给出明确原因,这时我们就需要对CNN决策进行可视化。这种方法还能帮助大家确定是否需要为训练数据集添加更具代表性的样本,或者判断数据集中是否存在偏差。例如,如果一套物体检测模型负责查找道路交通中的障碍物,但使用的训练数据集仅涵盖夏季周期内的采集样本,那么由于模型从未见过被冰雪覆盖的物体,因此可能无法在冬季场景下带来良好的推理结果。

在本文中,我们将部署一套用于交通标志分类的模型,并设置Amazon SageMaker Model Monitor以自动检测与预期不符的模型行为,例如始终较低的预测得分或者对某些图像类别的过度预测。当Model Monitor发现问题时,我们将使用Amazon SageMaker Debugger获取关于已部署模型的直观说明。大家可以更新推理端点,通过在推理期间发送张量,并使用这些张量计算出显著性映射。要重现本文中涉及的各种步骤与结果,请将amazon-sagemaker-analyze-model-predictions repo克隆至我们自己的Amazon SageMaker notebook实例或Amazon SageMaker Studio当中,而后运行该Notebook。

定义一个SageMaker模型

本文将使用经过训练的ResNet18模型,这套模型使用德国交通标志数据集[2] 以区分43种交通标志。当给定一幅输入图像时,该模型将输出不同图像类别的对应概率。每个类别对应一种不同的交通标志类型。我们已经对模型进行了调优,并将其权重上传至GitHub repo

在将模型部署至Amazon SageMaker之前,需要首先对模型权重进行归档,并上传至Amazon Simple Storage Service(Amazon S3)。请在Jupyter notebook单元中输入以下代码:

sagemaker_session.upload_data(path='model.tar.gz', key_prefix='model')

大家可以使用Amazon SageMaker托管服务来设置持久化的端点,通过这个端点从模型中获取预测结果。为此,我们需要定义一个PyTorch Model对象,负责从Amazon S3路径提取模型归档。定义一个entry_point file pretrained_model.py文件,实现model_fntransform_fn函数。大家可以在托管期间使用这些函数,保证模型已被正确加载至推理容器当中,且能够正确处理传入请求。具体参见以下代码:

from sagemaker.pytorch.model import PyTorchModel
model = PyTorchModel(model_data = 's3://' + sagemaker_session.default_bucket() + '/model/model.tar.gz',
 role = role,
 framework_version = '1.5.0',
 source_dir='entry_point',
 entry_point = 'pretrained_model.py',
 py_version='py3')

设置Model Monitor并部署模型

Model Monitor会自动监控生产环境中的机器学习模型,并在检测到数据质量问题时向您发出警报。在这套解决方案中,我们将捕捉端点的输入与输出,并创建监控计划以保证Model Monitor能够检查收集到的数据以及相关模型预测结果。我们使用DataCaptureConfig API指定Model Monitor在目标Amazon S3存储桶上存储模型输入与输出的比例。在以下示例中,采样百分比设置为50%:

from sagemaker.model_monitor import DataCaptureConfig
data_capture_config = DataCaptureConfig(
 enable_capture=True,
 sampling_percentage=50,
 destination_s3_uri='s3://' + sagemaker_session.default_bucket() + '/endpoint/data_capture'
)

要将端点部署至ml.m5.xlarge实例,请输入以下代码:

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

对测试图像进行推理

现在,大家可以使用包含序列化后的图像输入载荷调用该端点了。端点又将进一步调用transform_fn函数,在执行模型推理之前对数据进行预处理。该端点会以整数列表的形式返回图像的预测分类结果,然后编码为JSON格式,具体参见以下代码:

#invoke payload
response = runtime.invoke_endpoint(EndpointName=endpoint_name, Body=payload)
response_body = response['Body']
#get results
result = json.loads(response_body.read().decode())

现在,我们可以对测试图像及其预测出的分类结果进行可视化了。在以下可视化过程中,我们将交通标志图像作为待预测内容发送至该端点,上方标签则是从端点处接收到的相应预测。下图所示,为端点正确预测了类别23(湿滑路面)。
SageMaker Model Monitor与Debugger神助攻,理解卷积神经网络这个「黑匣子」_第2张图片

下图所示,为端点正确预测了类别25(道路施工)。
SageMaker Model Monitor与Debugger神助攻,理解卷积神经网络这个「黑匣子」_第3张图片

创建一项Model Monitor计划

接下来,我们将演示如何使用Model Monitor建立监控计划。Model Monitor提供一款内置容器用于创建基准,借此计算约束条件与统计数据,例如平均值、分位数以及标准差等等。以此为基础,我们可以启动监控计划,由该计划定期启动处理作业以检查收集到的数据、将数据与给定的约束条件进行比较,并可生成违规报告。

在本次用例当中,我们将创建一个自定义容器,该容器能够执行简单的模型完整性检查,即运行评估脚本、并由该脚本对预测的图像分类进行计数。如果模型对于特定交通标志的识别比例始终高于其他类别,或者置信度始终较低,则表明模型中存在问题。

例如,对于给定的输入图像,模型将返回基于置信度得分排名的预测类列表。如果前三项预测对应于毫不相关的三种类别,且每项预测的置信度得分均低于50%(例如第一项预测为停车标志,第二项为左转标志,第三项为限速180km/h标志),则三项结论恐怕都不能确切采信。

关于构建自定义容器并将其上传至Amazon Elastic Container Registry(Amazon ECR)的更多详细信息,请参阅Notebook。以下代码将创建一个Model Monitor对象,我们可以在其中指定Docker镜像在Amazon ECR上的位置,以及评估脚本所需要的环境变量。容器的入口点文件即为这套评估脚本。

monitor = ModelMonitor(
 role=role,
 image_uri='%s.dkr.ecr.us-west-2.amazonaws.com/sagemaker-processing-container:latest' %my_account_id,
 instance_count=1,
 instance_type='ml.m5.xlarge',
 env={'THRESHOLD':'0.5'}
)

接下来,定义一个Model Monitor计划并将其附加至端点当中。此计划每小时运行一次自定义容器,具体参见以下代码:

from sagemaker.model_monitor import CronExpressionGenerator
from sagemaker.processing import ProcessingInput, ProcessingOutput
destination = 's3://' + sagemaker_session.default_bucket() + '/endpoint/monitoring_schedule'
processing_output = ProcessingOutput(output_name='model_outputs', source='/opt/ml/processing/outputs', destination=destination)
output = MonitoringOutput(source=processing_output.source, destination=processing_output.destination)
monitor.create_monitoring_schedule(
 output=output,
 endpoint_input=predictor.endpoint,
 schedule_cron_expression=CronExpressionGenerator.hourly()
)

如前所述,脚本evaluation.py会执行简单的模型完整性检查,即对模型预测结果进行计数。Model Monitor在Amazon S3中将模型的输入与输出另存为JSON行格式的文件。这些文件将被下载至/opt/ml/processing/input的处理容器当中。接下来,大家可以通过'captureData'['data']加载这些预测结果,具体参见以下代码:

for file in files:
 content = open(file).read()
 for entry in content.split('n'):
 prediction = json.loads(entry)['captureData']['endpointOutput']['data']

我们可以在CloudWatch与SageMaker Studio中跟踪处理作业的状态。在以下截屏中,SageMaker Studio显示未发现任何问题。

捕获意外模型行为

在定义了计划之后,我们就可以实时监控模型了。若需要验证该设置能否正确捕捉意外行为,我们需要先执行错误的预测。为此,我们使用AdvBox工具包[3],其能够在像素层级加入干扰,保证模型无法继续识别出正确的类。这种干扰也被称为对抗攻击,其中的调整往往无法被人类所察觉。我们对几张被正确预测为停车标志的测试图像进行了转换。在以下图像集中,左侧图像为原始图像,中间为对抗图像,右侧为二者之间的差异。原始图像与对抗图像在直观上几乎没有任何区别,但对抗图像无法在推理过程中被正确分类。
SageMaker Model Monitor与Debugger神助攻,理解卷积神经网络这个「黑匣子」_第4张图片

下图所示,为另一项判断错误的交通标志:
SageMaker Model Monitor与Debugger神助攻,理解卷积神经网络这个「黑匣子」_第5张图片

在Model Monitor计划下一项处理作业时,它会分析那些已经捕捉并被存储在Amazon S3当中的预测结果。该作业会计算预测的图像类别;如果其中某一特定类别有超过50%的几率被选定为预测结果,则证明存在问题。由于我们已经将对抗图像发送至端点,因此大家现在可以看到图像在类别14(停止)上发生了异常计数。大家可以在SageMaker Studio中跟踪处理作业的状态。以下截屏中,SageMaker Studio显示在最后一项计划作业中发现了问题。
image

大家可以从Amazon CloudWatch Logs中获取更多详细信息:处理作业将输出一份字典,其中的键为43种图像类别之一,而值则为对应计数量。例如,在以下输出当中,端点认为图像类别9(禁止通行)出现了2次,而类别14(停止)则出现了反常的计数。在全部400次预测中,端点有322次做出了类别14的预测,已经高于50%的阈值水平。字典的值也被存储为CloudWatch指标,因此大家可以使用CloudWatch控制台为指标数据创建展示图表。

Warning: Class 14 ('Stop sign') predicted more than 80 % of the time which is above the threshold
Predicted classes {9: 2, 19: 2, 25: 1, 14: 322, 13: 5, 5: 1, 8: 10, 18: 1, 31: 4, 26: 8, 33: 4, 36: 4, 29: 20, 12: 8, 22: 4, 6: 4}

既然处理作业发现了问题,接下来肯定是要进一步了解真相了。观察前面的测试图像,我们发现原始图像与对抗图像之间没有显著差异。为了更好地理解模型「眼中」的内容,我们可以采用论文《Full-Gradient Representation for Neural Network Visualization》[1] 中提到技术。该技术能够对输入特征与中间特征映射(Feature map)的重要性整理出相应评分。在后文当中,我们将展示如何配置Debugger轻松抽取这些变量(张量),而无需修改模型本身。此外,我们还将详细介绍如何使用这些张量以计算显著性映射。

创建Debugger hook配置

要抽取张量,我们需要更新之前建立Amazon SageMaker PyTorch Model那一步中的预训练模型的Python脚本pretrained_model.py。我们在model_fn当中创建一项Debugger hook配置,该Hook定义一组正则表达式include_regex,这些正则表达式匹配的张量将会被收集。在后文中,我们将详细介绍如何计算显著性映射。此项计算需要使用中间层(例如BatchNorm以及下采样层)及模型输入的偏差与梯度。要获取张量,请指定以下正则表达式:

'.bn|.bias|.downsample|.ResNet_input|.*image'

将张量存储在Amazon SageMaker默认存储桶当中,具体请参见以下代码:

def model_fn(model_dir):
 #load model
 model = resnet.resnet18()
 model.load_state_dict(torch.load(model_dir))
 model.eval()
 #hook configuration
 save_config = smd.SaveConfig(mode_save_configs={
 smd.modes.PREDICT: smd.SaveConfigMode(save_interval=1)
 })
 hook = Hook("s3://" + sagemaker_session.default_bucket() + "tensors",
 save_config=save_config,
 include_regex='.*bn|.*bias|.*downsample|.*ResNet_input|.*image' )
 #register hook
 hook.register_module(model)
 #set mode
 hook.set_mode(modes.PREDICT)
 return model

使用新的入口点脚本pretrained_model_with_debugger_hook.py,创建一个新的PyTorch模型:

model = PyTorchModel(model_data = 's3://' + sagemaker_session.default_bucket() + '/model/model.tar.gz',
 role = role,
 framework_version = '1.3.1',
 source_dir='code',
 entry_point = 'pretrained_model_with_debugger_hook.py',
 py_version='py3')

使用新的PyTorch model对象更新现有端点,该对象使用经过修改、带有Debugger hook的模型脚本:

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

现在,每次做出推理请求时,端点都会记录张量并将结果上传至Amazon S3。现在,我们可以计算显著性映射,并从模型中获得直观的解释。

使用Debugger分析不正确预测

分类模型的输出通常为介于0和1之间的概率数组,其中各个条目对应于数据集中的标签。例如在MNIST(10个类别)的情况下,模型可能会对图像为数字8的输入预测为:[0.08, 0, 0, 0, 0, 0, 0.12, 0, 0.5, 0.3],表示图像被预测为0的概率为8%,6的概率为12%,8的概率为50%,9的概率为30%。要生成显著性映射,我们需要选择概率最高的类别(在本用例中,自然就是8),而后将得分映射回网络中的原有图层,借此识别做出这项预测的重要神经元。CNN由多个层组成,因此各个中间值的重要性得分将体现如何计算各个值对于当前预测结果的贡献。

大家可以将模型输入的预测结果梯度与输入进行比较,借此确定重要性得分。梯度代表着在输入变更时,输出会对应产生怎样程度的变化。要记录结果,请在层输出上注册一个反向Hook,并在推理过程中触发反向调用。我们已经配置了Debugger hook以捕捉相关的张量。

在更新端点并执行一些推理请求之后,大家可以创建一个trial对象,借此访问、查询并过滤Debugger保存的数据。具体请参见以下代码:

from smdebug.trials import create_trial
trial = create_trial('s3://' + sagemaker_session.default_bucket() + '/endpoint/tensors')

使用Debugger,我们可以通过trial.tensor().value()访问数据。例如,要获取第一条推理请求中第一个BatchNorm层的偏差张量,请输入以下代码:

trial.tensor('ResNet_bn1.bias').value(step_num=0, mode=modes.PREDICT).

函数trial.steps(mode=modes.PREDICT)会返回可用的步数,这里的步数对应于已记录的推理请求数。

在以下步骤中,我们将基于FullGrad方法计算显著性映射。这种方法将把输入梯度与特征层级的偏差梯度聚合起来。

计算隐性偏差

在FullGrad方法中,ResNet18的BatchNorm层会引入隐性偏差。大家可以通过获取运行平均值、方差与层权重的方式来计算隐性偏差,具体参见以下代码:

weight = trial.tensor(weight_name).value(step_num=step, mode=modes.PREDICT)
running_var = trial.tensor(running_var_name).value(step_num=step, mode=modes.PREDICT)
running_mean = trial.tensor(running_mean_name).value(step_num=step, mode=modes.PREDICT)
implicit_bias = - running_mean / np.sqrt(running_var) * weight

梯度偏差相乘

偏差是指显性与隐性偏差的总和。大家可以获取特征映射的输出梯度,并计算偏差与梯度的乘积,具体参见以下代码:

gradient = trial.tensor(gradient_name).value(step_num=step, mode=modes.PREDICT)
bias = trial.tensor(bias_name).value(step_num=step, mode=modes.PREDICT)
bias = bias + implicit_bias
bias_gradient = normalize(np.abs(bias * gradient))

插值与聚合

中间层的维度通常与输入图像不同,因此我们需要对其进行插值。大家可以针对所有偏差梯度执行插值操作,并汇总相应结果。其总和即为我们以热图形式叠加在原始输入图像上的显著性映射,具体参见以下代码:

for channel in range(bias_gradient.shape[1]):
interpolated = scipy.ndimage.zoom(bias_gradient[0,channel,:,:], image_size/bias_gradient.shape[2], order=1)

saliency_map += interpolated

结果

在本节中,我们引入了一些对抗图像示例,模型将这些图像识别为停止标志。右侧图像显示的是模型输入图像与显著性映射的叠加结果。红色代表在模型预测中影响最大的部分,同时也很可能代表着像素干扰所处的位置。可以看到,在干扰影响之下,模型不再能正确考虑到相关的对象特征,而且提供的大多数预测的置信度得分也很低。
SageMaker Model Monitor与Debugger神助攻,理解卷积神经网络这个「黑匣子」_第6张图片

SageMaker Model Monitor与Debugger神助攻,理解卷积神经网络这个「黑匣子」_第7张图片

SageMaker Model Monitor与Debugger神助攻,理解卷积神经网络这个「黑匣子」_第8张图片

为了进行比较,我们还使用原始(非对抗)图像进行了推理。在以下图像集中,左侧图像为类别「停止」所对应的对抗图像与显著性映射。右侧图像则为正确类判断所对应的原始输入图像(非对抗图像)与显著性映射。在非对抗图像当中,模型明显仅关注相关的对象特征,因此更有可能做出正确的图像类别预测。而在处理对抗图像时,模型考虑到相关对象之外的众多其他特征,而这明显是由随机像素干扰所引起。
SageMaker Model Monitor与Debugger神助攻,理解卷积神经网络这个「黑匣子」_第9张图片

总结

本文展示了如何使用Amazon SageMaker Model Monitor与Amazon SageMaker Debugger自动检测意外模型行为,并从CNN中获取直观解释。关于更多详细信息,请参阅GitHub repo

参考文献

[1] Suraj Srinivas, Francois Fleuret,《神经网络可视化的全梯度表示》(Full-gradient representation for neural network visualization), Advances in Neural Information Processing Systems (NeurIPS), 2019年

[2] Johannes Stallkamp, Marc Schlipsing, Jan Salmen, Christian Igel,《德国交通标志识别基准:多类分类竞赛》(The German traffic sign recognition benchmark: A multi-class classification competition,) 2011年国际神经网络联合会议,2011年

[3] Dou Goodman, Hao Xin, Wang Yang, Wu Yuesheng, Xiong Junfeng, Zhang Huan,《Advbox:一款用于生成对抗示例以干扰神经网络判断的工具箱》(Advbox: a toolbox to generate adversarial examples that fool neural networks
image

你可能感兴趣的:(人工智能,机器学习)