TensorBoard 性能分析: 在 Keras 中对基本训练指标进行性能分析

在 TensorFlow.org 上查看 在 Google Colab 上运行 在 GitHub 上查看源代码 下载此 notebook

总览

在机器学习中性能十分重要。TensorFlow 有一个内置的性能分析器可以使您不用费力记录每个操作的运行时间。然后您就可以在 TensorBoard 的 Profile Plugin 中对配置结果进行可视化。本教程侧重于 GPU ,但性能分析插件也可以按照云 TPU 工具来在 TPU 上使用。

本教程提供了非常基础的示例以帮助您学习如何在开发 Keras 模型时启用性能分析器。您将学习如何使用 Keras TensorBoard 回调函数来可视化性能分析结果。“其他性能分析方式”中提到的 Profiler APIProfiler Server 允许您分析非 Keras TensorFlow 的任务。

事先准备

  • 在你的本地机器上安装最新的TensorBoard

  • 在 Notebook 设置的加速器的下拉菜单中选择 “GPU”(假设您在Colab上运行此notebook)

Notebook 设置

设置

try:
  # %tensorflow_version 只在 Colab 中存在。
  %tensorflow_version 2.x
except Exception:
  pass

# 加载 TensorBoard notebook 扩展。
%load_ext tensorboard

TensorFlow 2.x selected.

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

from datetime import datetime
from packaging import version

import functools
import tensorflow as tf
import tensorflow_datasets as tfds
from tensorflow.python.keras import backend
from tensorflow.python.keras import layers

import numpy as np

print("TensorFlow version: ", tf.__version__)

TensorFlow version:  2.0.0-dev20190424

确认 TensorFlow 可以看到 GPU。

device_name = tf.test.gpu_device_name()
if not tf.test.is_gpu_available():
  raise SystemError('GPU device not found')
print('Found GPU at: {}'.format(device_name))
Found GPU at: /device:GPU:0

使用 TensorBoard callback 运行一个简单的模型

你将使用 Keras 来构建一个使用 ResNet56 (参考: 用于图像识别的深度残差学习)来分类CIFAR-10图像集的简单模型。

TensorFlow 模型园复制 ResNet 模型代码。

BATCH_NORM_DECAY = 0.997
BATCH_NORM_EPSILON = 1e-5
L2_WEIGHT_DECAY = 2e-4


def identity_building_block(input_tensor,
                            kernel_size,
                            filters,
                            stage,
                            block,
                            training=None):

  """标识块是一种在捷径上没有卷积层的块。

  参数:
    input_tensor:输入张量
    kernel_size:默认为3,内核大小为
        主路径上的中间卷积层
    过滤器:整数列表,主路径上3个卷积层的过滤器
    stage:整数,当前阶段标签,用于生成层名称
    block:当前块标签,用于生成层名称
    training:仅在使用 Estimator 训练 keras 模型时使用。 在其他情况下,它是自动处理的。

  返回值:
    输出块的张量。
  """
  filters1, filters2 = filters
  if tf.keras.backend.image_data_format() == 'channels_last':
    bn_axis = 3
  else:
    bn_axis = 1
  conv_name_base = 'res' + str(stage) + block + '_branch'
  bn_name_base = 'bn' + str(stage) + block + '_branch'

  x = tf.keras.layers.Conv2D(filters1, kernel_size,
                             padding='same',
                             kernel_initializer='he_normal',
                             kernel_regularizer=
                             tf.keras.regularizers.l2(L2_WEIGHT_DECAY),
                             bias_regularizer=
                             tf.keras.regularizers.l2(L2_WEIGHT_DECAY),
                             name=conv_name_base + '2a')(input_tensor)
  x = tf.keras.layers.BatchNormalization(axis=bn_axis,
                                         name=bn_name_base + '2a',
                                         momentum=BATCH_NORM_DECAY,
                                         epsilon=BATCH_NORM_EPSILON)(
                                             x, training=training)
  x = tf.keras.layers.Activation('relu')(x)

  x = tf.keras.layers.Conv2D(filters2, kernel_size,
                             padding='same',
                             kernel_initializer='he_normal',
                             kernel_regularizer=
                             tf.keras.regularizers.l2(L2_WEIGHT_DECAY),
                             bias_regularizer=
                             tf.keras.regularizers.l2(L2_WEIGHT_DECAY),
                             name=conv_name_base + '2b')(x)
  x = tf.keras.layers.BatchNormalization(axis=bn_axis,
                                         name=bn_name_base + '2b',
                                         momentum=BATCH_NORM_DECAY,
                                         epsilon=BATCH_NORM_EPSILON)(
                                             x, training=training)

  x = tf.keras.layers.add([x, input_tensor])
  x = tf.keras.layers.Activation('relu')(x)
  return x


def conv_building_block(input_tensor,
                        kernel_size,
                        filters,
                        stage,
                        block,
                        strides=(2, 2),
                        training=None):
  """在捷径中具有卷积层的块。

  参数:
    input_tensor:输入张量
    kernel_size:默认为3,内核大小为
        主路径上的中间卷积层
    filters:整数列表,主路径上3个卷积层的过滤器
    stage:整数,当前阶段标签,用于生成层名称
    block:当前块标签,用于生成层名称
    training:仅在使用 Estimator 训练 keras 模型时使用。在其他情况下,它是自动处理的。

  返回值:
    输出块的张量。

  请注意,从第3阶段开始,
  主路径上的第一个卷积层的步长=(2,2)
  而且捷径的步长=(2,2)
  """
  filters1, filters2 = filters
  if tf.keras.backend.image_data_format() == 'channels_last':
    bn_axis = 3
  else:
    bn_axis = 1
  conv_name_base = 'res' + str(stage) + block + '_branch'
  bn_name_base = 'bn' + str(stage) + block + '_branch'

  x = tf.keras.layers.Conv2D(filters1, kernel_size, strides=strides,
                             padding='same',
                             kernel_initializer='he_normal',
                             kernel_regularizer=
                             tf.keras.regularizers.l2(L2_WEIGHT_DECAY),
                             bias_regularizer=
                             tf.keras.regularizers.l2(L2_WEIGHT_DECAY),
                             name=conv_name_base + '2a')(input_tensor)
  x = tf.keras.layers.BatchNormalization(axis=bn_axis,
                                         name=bn_name_base + '2a',
                                         momentum=BATCH_NORM_DECAY,
                                         epsilon=BATCH_NORM_EPSILON)(
                                             x, training=training)
  x = tf.keras.layers.Activation('relu')(x)

  x = tf.keras.layers.Conv2D(filters2, kernel_size, padding='same',
                             kernel_initializer='he_normal',
                             kernel_regularizer=
                             tf.keras.regularizers.l2(L2_WEIGHT_DECAY),
                             bias_regularizer=
                             tf.keras.regularizers.l2(L2_WEIGHT_DECAY),
                             name=conv_name_base + '2b')(x)
  x = tf.keras.layers.BatchNormalization(axis=bn_axis,
                                         name=bn_name_base + '2b',
                                         momentum=BATCH_NORM_DECAY,
                                         epsilon=BATCH_NORM_EPSILON)(
                                             x, training=training)

  shortcut = tf.keras.layers.Conv2D(filters2, (1, 1), strides=strides,
                                    kernel_initializer='he_normal',
                                    kernel_regularizer=
                                    tf.keras.regularizers.l2(L2_WEIGHT_DECAY),
                                    bias_regularizer=
                                    tf.keras.regularizers.l2(L2_WEIGHT_DECAY),
                                    name=conv_name_base + '1')(input_tensor)
  shortcut = tf.keras.layers.BatchNormalization(
      axis=bn_axis, name=bn_name_base + '1',
      momentum=BATCH_NORM_DECAY, epsilon=BATCH_NORM_EPSILON)(
          shortcut, training=training)

  x = tf.keras.layers.add([x, shortcut])
  x = tf.keras.layers.Activation('relu')(x)
  return x


def resnet_block(input_tensor,
                 size,
                 kernel_size,
                 filters,
                 stage,
                 conv_strides=(2, 2),
                 training=None):
  """一个应用层后跟多个标识块的块。

  参数:
    input_tensor:输入张量
    size:整数,构成转化卷积/身份块的数量。
    一个卷积层使用后,再跟(size-1)个身份块。
    kernel_size:默认为3,内核大小为
        主路径上的中间卷积层
    filters:整数列表,主路径上3个卷积层的过滤器
    stage:整数,当前阶段标签,用于生成层名称
    conv_strides:块中第一个卷积层的步长。
    training:仅在使用 Estimator 训练 keras 模型时使用。其他情况它会自动处理。  

  返回值:
    应用层和身份块后的输出张量。
  """

  x = conv_building_block(input_tensor, kernel_size, filters, stage=stage,
                          strides=conv_strides, block='block_0',
                          training=training)
  for i in range(size - 1):
    x = identity_building_block(x, kernel_size, filters, stage=stage,
                                block='block_%d' % (i + 1), training=training)
  return x

def resnet(num_blocks, classes=10, training=None):
  """实例化ResNet体系结构。

  参数:
    num_blocks:整数,每个块中的卷积/身份块的数量。
      ResNet 包含3个块,每个块包含一个卷积块
      后面跟着(layers_per_block - 1) 个身份块数。 每
      卷积/理想度块具有2个卷积层。 用输入
      卷积层和池化层至最后,这带来了
      网络的总大小为(6 * num_blocks + 2)
    classes:将图像分类为的可选类数
    training:仅在使用 Estimator 训练 keras 模型时使用。其他情况下它会自动处理。

  返回值:
    Keras模型实例。
  """

  input_shape = (32, 32, 3)
  img_input = layers.Input(shape=input_shape)

  if backend.image_data_format() == 'channels_first':
    x = layers.Lambda(lambda x: backend.permute_dimensions(x, (0, 3, 1, 2)),
                      name='transpose')(img_input)
    bn_axis = 1
  else:  # channel_last
    x = img_input
    bn_axis = 3

  x = tf.keras.layers.ZeroPadding2D(padding=(1, 1), name='conv1_pad')(x)
  x = tf.keras.layers.Conv2D(16, (3, 3),
                             strides=(1, 1),
                             padding='valid',
                             kernel_initializer='he_normal',
                             kernel_regularizer=
                             tf.keras.regularizers.l2(L2_WEIGHT_DECAY),
                             bias_regularizer=
                             tf.keras.regularizers.l2(L2_WEIGHT_DECAY),
                             name='conv1')(x)
  x = tf.keras.layers.BatchNormalization(axis=bn_axis, name='bn_conv1',
                                         momentum=BATCH_NORM_DECAY,
                                         epsilon=BATCH_NORM_EPSILON)(
                                             x, training=training)
  x = tf.keras.layers.Activation('relu')(x)

  x = resnet_block(x, size=num_blocks, kernel_size=3, filters=[16, 16],
                   stage=2, conv_strides=(1, 1), training=training)

  x = resnet_block(x, size=num_blocks, kernel_size=3, filters=[32, 32],
                   stage=3, conv_strides=(2, 2), training=training)

  x = resnet_block(x, size=num_blocks, kernel_size=3, filters=[64, 64],
                   stage=4, conv_strides=(2, 2), training=training)

  x = tf.keras.layers.GlobalAveragePooling2D(name='avg_pool')(x)
  x = tf.keras.layers.Dense(classes, activation='softmax',
                            kernel_initializer='he_normal',
                            kernel_regularizer=
                            tf.keras.regularizers.l2(L2_WEIGHT_DECAY),
                            bias_regularizer=
                            tf.keras.regularizers.l2(L2_WEIGHT_DECAY),
                            name='fc10')(x)

  inputs = img_input
  # 创建模型
  model = tf.keras.models.Model(inputs, x, name='resnet56')

  return model


resnet20 = functools.partial(resnet, num_blocks=3)
resnet32 = functools.partial(resnet, num_blocks=5)
resnet56 = functools.partial(resnet, num_blocks=9)
resnet110 = functools.partial(resnet, num_blocks=18)

TensorFlow 数据集下载 CIFAR-10 数据集。

cifar_builder = tfds.builder('cifar10')
cifar_builder.download_and_prepare()

建立数据输入线性通信模型并编译 ResNet56 模型。

HEIGHT = 32
WIDTH = 32
NUM_CHANNELS = 3
NUM_CLASSES = 10
BATCH_SIZE = 128

def preprocess_data(record):
  image = record['image']
  label = record['label']

  # 调整图像大小以在每侧增加四个额外的像素。
  image = tf.image.resize_with_crop_or_pad(
      image, HEIGHT + 8, WIDTH + 8)

  # 随机裁剪图像的 [HEIGHT,WIDTH] 部分。
  image = tf.image.random_crop(image, [HEIGHT, WIDTH, NUM_CHANNELS])

  # 随机水平翻转图像。
  image = tf.image.random_flip_left_right(image)

  # 减去均值并除以像素方差。
  image = tf.image.per_image_standardization(image)

  label = tf.compat.v1.sparse_to_dense(label, (NUM_CLASSES,), 1)
  return image, label

train_data = cifar_builder.as_dataset(split=tfds.Split.TRAIN)
train_data = train_data.repeat()
train_data = train_data.map(
    lambda value: preprocess_data(value))
train_data = train_data.shuffle(1024)

train_data = train_data.batch(BATCH_SIZE)

model = resnet56(classes=NUM_CLASSES)

model.compile(optimizer='SGD',
              loss='categorical_crossentropy',
              metrics=['categorical_accuracy'])

当你创建 TensorBoard 回调时,您可以指定您想要进行性能分析的批次。默认情况下,TensorFlow 将对第二个批次进行性能分析,因为第一个批次的时候会运行很多一次性的图优化。您可以通过设置 profile_batch 对其进行修改。您还可以通过将其设置为 0 来关闭性能分析。

这时候,您将会对第三批次进行性能分析。

log_dir="logs/profile/" + datetime.now().strftime("%Y%m%d-%H%M%S")

tensorboard_callback = tf.keras.callbacks.TensorBoard(log_dir=log_dir, histogram_freq=1, profile_batch = 3)

开始使用 Model.fit() 进行训练。

model.fit(train_data,
          steps_per_epoch=20,
          epochs=5, 
          callbacks=[tensorboard_callback])
Epoch 1/5
 1/20 [>.............................] - ETA: 14:27 - loss: 5.4251 - categorical_accuracy: 0.0859
W0425 21:14:50.396199 140078590396288 callbacks.py:238] Method (on_train_batch_end) is slow compared to the batch update (0.317050). Check your callbacks.

 2/20 [==>...........................] - ETA: 6:58 - loss: 5.5955 - categorical_accuracy: 0.0781 
W0425 21:14:50.954807 140078590396288 callbacks.py:238] Method (on_train_batch_end) is slow compared to the batch update (0.268180). Check your callbacks.

 3/20 [===>..........................] - ETA: 4:26 - loss: 5.7003 - categorical_accuracy: 0.0911
W0425 21:14:51.180765 140078590396288 callbacks.py:238] Method (on_train_batch_end) is slow compared to the batch update (0.134130). Check your callbacks.

20/20 [==============================] - 51s 3s/step - loss: 5.3766 - categorical_accuracy: 0.1004
Epoch 2/5
20/20 [==============================] - 5s 227ms/step - loss: 4.8007 - categorical_accuracy: 0.0988
Epoch 3/5
20/20 [==============================] - 5s 242ms/step - loss: 4.3439 - categorical_accuracy: 0.0980
Epoch 4/5
20/20 [==============================] - 5s 247ms/step - loss: 3.9405 - categorical_accuracy: 0.1074
Epoch 5/5
20/20 [==============================] - 5s 225ms/step - loss: 3.6195 - categorical_accuracy: 0.1176

<tensorflow.python.keras.callbacks.History at 0x7f65f8758908>

使用 TensorBoard 可视化性能分析结果

不幸的是,由于#1913, 您无法在 Colab 中使用 TensorBoard 来可视化性能分析结果。您需要下载日志目录并在本地计算机上启动 TensorBoard。

压缩下载日志:

tar -zcvf logs.tar.gz logs/profile/

在“文件”选项卡中右键单击以下载 logdir.tar.gz

下载

请保证在你本地的机器安装最新的 TensorBoard。在你的本地机器上执行下面的命令:

> cd download/directory
> tar -zxvf logs.tar.gz
> tensorboard --logdir=logs/ --port=6006

在您的Chrome浏览器中打开一个新标签,然后导航至localhost:6006,单击 “Profile” 标签。您可能会看到以下性能分析结果:

跟踪视图

跟踪查看器

当您单击性能分析选项卡后,您将看到跟踪查看器。该页面显示了聚合期间 CPU 和加速器上发生的不同事件的时间轴。

跟踪查看器在垂直轴上显示多个 事件组。 每个事件组都有多个水平 跟踪,其中填充了跟踪事件。跟踪 事件是在线程或 GPU 流上执行的基本时间线,。单个事件是时间轴轨道上的彩色矩形块。时间从左到右移动。

您可以使用 w(放大),s(缩小),a(向左滚动),d(向右滚动)浏览结果。

单个矩形代表 跟踪事件 :从这个时间的开始到结束时间。 要研究单个矩形,可以在浮动工具栏中选择鼠标光标图标后单击它。 这将显示有关矩形的信息,例如其开始时间和持续时间。

除了点击之外,您还可以拖动鼠标以选择覆盖一组跟踪事件的矩形。这将为您提供与该矩形相交并汇总的事件列表。 m 键可用于测量所选事件的持续时间。

List of Events

跟踪事件是从三个来源收集的:

  • CPU: CPU事件位于名为/host:CPU的事件组下。每个轨道代表 CPU 上的一个线程。例如,输入线性通信模型事件,GPU 操作调度事件, CPU 操作执行事件等。
  • GPU: GPU 事件位于以 /device:GPU:为前缀的事件组下。 除了 stream:all,每个事件组都代表在 GPU 上一个流。 stream::all将所有事件汇集到一个 GPU 上。例如。 内存复制事件,内核执行事件等。
  • TensorFlow 运行时间: 运行时事件在以 /job:为前缀的事件组下。运行事件表示 python 程序调用的 TensorFlow ops。 例如, tf.function 执行事件等。

调试性能

现在,您将使用 Trace Viewer 来改善您的模型的性能。

让我们回到刚刚捕获的分析结果。

GPU kernel

GPU 事件表明,GPU 在该步骤的上半部分什么都没有做。

CPU events

CPU 事件表明,在此步骤的开始的时候,CPU 被数据输入管道占用。

Runtime

在 TensorFlow 运行时中,有一个叫 Iterator::GetNextSync的大阻塞,这是从数据输入管道中获取下一批的阻塞调用。而且它阻碍了训练步骤。 因此,如果您可以在 s-1 的时候为 s 步骤准备输入数据,则可以更快地训练该模型。

您也可以通过使用 tf.data.prefetch.

train_data = cifar_builder.as_dataset(split=tfds.Split.TRAIN)
train_data = train_data.repeat()
train_data = train_data.map(
    lambda value: preprocess_data(value))
train_data = train_data.shuffle(1024)
train_data = train_data.batch(BATCH_SIZE)

# 它将在(s-1)步骤中预取数据
train_data = train_data.prefetch(buffer_size=tf.data.experimental.AUTOTUNE)

重新运行模型。

log_dir="logs/profile/" + datetime.now().strftime("%Y%m%d-%H%M%S")

tensorboard_callback = tf.keras.callbacks.TensorBoard(log_dir=log_dir, histogram_freq=1, profile_batch = 3)

model.fit(train_data,
          steps_per_epoch=20,
          epochs=5, 
          callbacks=[tensorboard_callback])
Epoch 1/5
20/20 [==============================] - 5s 265ms/step - loss: 3.4081 - categorical_accuracy: 0.1055
Epoch 2/5
20/20 [==============================] - 4s 205ms/step - loss: 3.3122 - categorical_accuracy: 0.1141
Epoch 3/5
20/20 [==============================] - 4s 200ms/step - loss: 3.2795 - categorical_accuracy: 0.1199
Epoch 4/5
20/20 [==============================] - 4s 204ms/step - loss: 3.2237 - categorical_accuracy: 0.1469
Epoch 5/5
20/20 [==============================] - 4s 201ms/step - loss: 3.1888 - categorical_accuracy: 0.1465

<tensorflow.python.keras.callbacks.History at 0x7f65fbf87898>

Woohoo! 你刚刚把训练性能从 ~235ms/step 提高到 ~200ms/step

tar -zcvf logs.tar.gz logs/profile/

再一次下载 logs 目录来查看 TensorBoard的新的分析结果。

TF Runtime

Iterator::GetNextSync大阻塞不再存在。

做得好!

显然,这依旧不是最佳性能。请自己尝试,看看是否可以有更多的改进。

有关性能调整的一些有用参考:

其他分析方式

除了 TensorBoard 回调外,TensorFlow 还提供了其他两种方式来手动触发分析器:Profiler APIsProfiler Service

注意:请不要同时运行多个分析器。如果您想将 Profiler API 或 Profiler Service 与 TensorBoard 回调一起使用,请确保将profile_batch 参数设置为0。

Profiler APIs

# 内容管理接口
with tf.python.eager.profiler.Profiler('logdir_path'):
  # 进行你的训练
  pass


# 功能接口
tf.python.eager.profiler.start()
# 进行你的训练
profiler_result = tf.python.eager.profiler.stop()
tf.python.eager.profiler.save('logdir_path', profiler_result)

Profiler Service

# 此 API 将在您的 TensorFlow 作业上启动 gRPC 服务器,该 API 可以按需接收分析请求。
tf.python.eager.profiler.start_profiler_server(6009)

# 在这里写你的 TensorFlow 项目

然后,您可以单击 “Capture Profile” 按钮将性能分析请求发送到 Profiler 服务器以在 TensorBoard 上执行按需分析:

CAPTURE PROFILE

成功捕获后将显示一条消息。 然后,您可以刷新TensorBoard来获得可视化结果。