tf2-pkt-ref-merge-2

165 阅读43分钟

TensorFlow2 口袋参考(三)

原文:PyTorch Pocket Reference

译者:飞龙

协议:CC BY-NC-SA 4.0

第六章:模型创建风格

正如您可能想象的那样,构建深度学习模型有多种方法。在前几章中,您了解了tf.keras.Sequential,被称为符号 API,通常是教授模型创建的起点。您可能遇到的另一种 API 风格是命令式 API。符号 API 和命令式 API 都能够构建深度学习模型。

总的来说,选择哪种 API 取决于风格。根据您的编程经验和背景,其中一种可能对您来说更自然。在本章中,您将学习如何使用两种 API 构建相同的模型。具体来说,您将学习如何使用CIFAR-10 图像数据集构建图像分类模型。该数据集包含 10 种常见的类别或图像类别。与之前使用的花卉图像一样,CIFAR-10 图像作为 TensorFlow 分发的一部分提供。然而,花卉图像是 JPEG 格式,而 CIFAR-10 图像是 NumPy 数组。为了将它们流式传输到训练过程中,您将使用from_tensor_slices方法,而不是像在第五章中所做的flow_from_directory方法。

通过使用from_tensor_slices建立数据流程后,您将首先使用符号 API 构建和训练图像分类模型,然后使用命令式 API。您将看到,无论如何构建模型架构,结果都非常相似。

使用符号 API

您已经在本书的示例中看到了符号 APItf.keras.Sequential的工作原理。在tf.keras.Sequential中有一堆层,每个层对输入数据执行特定操作。由于模型是逐层构建的,这是一种直观的方式来设想这个过程。在大多数情况下,您只有一个输入源(在本例中是一系列图像),输出是输入图像的类别。在“使用 TensorFlow Hub 实现模型”中,您学习了如何使用 TensorFlow Hub 构建模型。模型架构是使用顺序 API 定义的,如图 6-1 所示。

顺序 API 模式和数据流

图 6-1。顺序 API 模式和数据流

在本节中,您将学习如何使用此 API 构建和训练一个使用 CIFAR-10 图像的图像分类模型。

加载 CIFAR-10 图像

CIFAR-10 图像数据集包含 10 个类别:飞机、汽车、鸟、猫、鹿、狗、青蛙、马、船和卡车。所有图像大小为 32×32 像素,带有三个通道(RGB)。

首先导入必要的库:

import tensorflow as tf
from tensorflow.keras import datasets, layers, models
import numpy as np
import matplotlib.pylab as plt

(train_images, train_labels), (test_images, test_labels) = 
datasets.cifar10.load_data()

此代码将 CIFAR-10 图像下载到您的 Python 运行时中,分为训练集和测试集。您可以使用type语句验证格式:

print(type(train_images))

输出将是一个数据类型:

<class 'numpy.ndarray'>

还重要的是要知道数组的形状,您可以使用以下命令找到:

print(train_images.shape, train_labels.shape)

以下是图像和标签的数组形状:

(50000, 32, 32, 3) (50000, 1)

您可以对测试数据执行相同的操作:

print(test_images.shape, test_labels.shape)

您应该得到以下输出:

(10000, 32, 32, 3) (10000, 1)

从输出中可以看出,CIFAR-10 数据集包含 50,000 个训练图像,每个图像大小为 32×32×3 像素。伴随的 50,000 个标签是一个一维数组,表示图像类别的索引。同样,还有 10,000 个测试图像和相应的标签。标签索引对应以下名称:

CLASS_NAMES = ['airplane', 'automobile', 'bird', 'cat',
               'deer', 'dog', 'frog', 'horse', 'ship', 'truck']

因此,索引 0 表示标签“飞机”,而索引 9 表示“卡车”。

检查标签分布

现在是时候找出这些类别的分布并查看一些图像了。通过查看训练标签的分布,可以了解每个类别有多少样本,使用 NumPy 的unique函数:

unique, counts = np.unique(train_labels, return_counts=True)

这将返回每个标签的样本计数。要显示它:

print(np.asarray((unique, counts)))

它将显示以下内容:

[[ 0 1 2 3 4 5 6 7 8 9]
 [5000 5000 5000 5000 5000 5000 5000 5000 5000 5000]]

这意味着每个标签(类)中有 5,000 张图片。训练数据在所有标签之间均匀分布。

同样,您可以验证测试数据的分布:

unique, counts = np.unique(test_labels, return_counts=True)
print(np.asarray((unique, counts)))

输出确认了每个标签有 1,000 张图片:

[[ 0 1 2 3 4 5 6 7 8 9]
 [1000 1000 1000 1000 1000 1000 1000 1000 1000 1000]]

检查图像

让我们看一些图像,以确保它们的数据质量。在这个练习中,您将随机抽样并显示训练数据集中的 50,000 张图像中的 25 张。

TensorFlow 如何进行这种随机选择?图像从 0 到 49,999 进行索引。要从此范围中随机选择有限数量的索引,使用 Python 的random库,该库以 Python 列表作为输入,并从中随机选择有限数量的样本:

selected_elements = random.sample(a_list, 25)

此代码从a_list中随机选择 25 个元素,并将结果存储在selected_elements中。如果a_list对应于图像索引,则selected_elements将包含从a_list中随机抽取的 25 个索引。您将使用selected_elements来访问和显示这 25 张训练图像。

现在您需要创建train_idx,该列表保存训练图像的索引。您将使用 Python 的range函数创建一个包含 0 到 49,999 之间整数的对象:

range(len(train_labels))

前面的代码创建了一个range对象,其中包含从 0 开始到len(train_labels)或列表training_labels的长度的整数。

现在,将range对象转换为 Python 列表:

list(range(len(train_labels)))

这个列表现在已准备好作为 Pythonrandom.sample函数的输入。现在您可以开始编写代码了。

首先,创建train_idx,这是一个从 0 到 49,999 的索引列表:

train_idx = list(range(len(train_labels)))

然后使用random库生成随机选择:

import random
random.seed(2)
random_sel = random.sample(train_idx, 25)

第二行中的种子操作确保您的选择是可重现的,这对于调试很有帮助。您可以为seed使用任何整数。

random_sel列表将保存 25 个随机选择的索引,看起来像这样:

[3706,
 6002,
 5562,
 23662,
 11081,
 48232,
…

现在您可以根据这些索引绘制图像并显示它们的标签:

plt.figure(figsize=(10,10))
for i in range(len(random_sel)):
 plt.subplot(5,5,i+1)
 plt.xticks([])
 plt.yticks([])
 plt.grid(False)
 plt.imshow(train_images[random_sel[i]], cmap=plt.cm.binary)
 plt.xlabel(CLASS_NAMES[train_labels[random_sel[i]][0]])
plt.show()

此代码片段显示了一个包含 25 张图像及其标签的面板,如图 6-2 所示。(由于这是一个随机样本,您的结果会有所不同。)

从 CIFAR-10 数据集中随机选择的 25 张图像

图 6-2。从 CIFAR-10 数据集中随机选择的 25 张图像

构建数据管道

在本节中,您将使用from_tensor_slices构建数据摄入管道。由于只有两个分区,训练和测试,您需要在训练过程中将测试分区的一半保留为交叉验证。选择前 500 个作为交叉验证数据,剩下的 500 个作为测试数据:

validation_dataset = tf.data.Dataset.from_tensor_slices(
(test_images[:500], test_labels[:500]))

test_dataset = tf.data.Dataset.from_tensor_slices(
(test_images[500:], test_labels[500:]))

此代码基于图像索引创建了两个数据集对象,validation_datasettest_dataset,每个集合中有 500 个样本。

现在为训练数据创建一个类似的数据集对象:

train_dataset = tf.data.Dataset.from_tensor_slices(
(train_images, train_labels))

这里使用了所有的训练图像。您可以通过计算train_dataset中的样本数量来确认:

train_dataset_size = len(list(train_dataset.as_numpy_iterator()))
print('Training data sample size: ', train_dataset_size)

这是预期结果:

Training data sample size: 50000

为训练批处理数据

要完成用于训练的数据摄入管道的设置,您需要将训练数据划分为批次。批次的大小,或训练样本的数量,是模型训练过程中更新模型权重和偏差所需的数量,然后沿着一步减少误差梯度。

使用以下代码对训练数据进行批处理,首先对训练数据集进行洗牌,然后创建多个包含 200 个样本的批次:

TRAIN_BATCH_SIZE = 200
train_dataset = train_dataset.shuffle(50000).batch(
TRAIN_BATCH_SIZE)

同样,您将对交叉验证和测试数据执行相同的操作:

validation_dataset = validation_dataset.batch(500)
test_dataset = test_dataset.batch(500)

STEPS_PER_EPOCH = train_dataset_size // TRAIN_BATCH_SIZE
VALIDATION_STEPS = 1 #validation data // validation batch size

交叉验证和测试数据集各包含一个 500 样本批次。代码设置参数以通知训练过程应该期望多少批次的训练和验证数据。训练数据的参数是STEPS_PER_EPOCH。交叉验证数据的参数是VALIDATION_STEPS,设置为 1,因为数据大小和批次大小都是 500。请注意,双斜杠(//)表示地板除法(即向下取整到最接近的整数)。

现在您的训练和验证数据集已经准备好了,下一步是使用符号 API 构建模型。

构建模型

现在您已经准备好构建模型了。以下是一个使用tf.keras.Sequential类包装的一堆层构建的深度学习图像分类模型的示例代码:

model = tf.keras.Sequential([
 tf.keras.layers.Conv2D(32, kernel_size=(3, 3), activation='relu',
 kernel_initializer='glorot_uniform', padding='same', 
 input_shape = (32,32,3)),
 tf.keras.layers.MaxPooling2D(pool_size=(2, 2)),
 tf.keras.layers.Conv2D(64, kernel_size=(3, 3), activation='relu',
 kernel_initializer='glorot_uniform', 
 padding='same'),
 tf.keras.layers.MaxPooling2D(pool_size=(2, 2)),
 tf.keras.layers.Flatten(),
 tf.keras.layers.Dense(256, activation='relu', 
 kernel_initializer='glorot_uniform'),
 tf.keras.layers.Dense(10, activation='softmax', 
 name = 'custom_class')
])
model.build([None, 32, 32, 3])

接下来,使用为分类任务指定的损失函数编译模型:

model.compile(
 loss='sparse_categorical_crossentropy',
 optimizer=tf.keras.optimizers.SGD(learning_rate=0.1, 
 momentum=0.9),
 metrics=['accuracy'])

为了想象模型如何通过不同层处理和转换数据,您可能希望绘制模型架构,包括它期望的张量的输入和输出形状。您可以使用以下命令:

tf.keras.utils.plot_model(model, show_shapes=True)

在运行此命令之前,您可能需要安装pydotgraphviz库:

pip install pydot
pip install graphviz

图 6-3 展示了模型架构。问号表示表示样本大小的维度,在执行期间才知道。这是因为模型设计为适用于任何大小的训练样本。处理样本大小所需的内存是无关紧要的,不需要在模型架构级别指定。相反,所需的内存将在训练执行期间定义。

接下来,开始训练过程:

hist = model.fit(
 train_dataset,
 epochs=5, steps_per_epoch=STEPS_PER_EPOCH,
 validation_data=validation_dataset,
 validation_steps=VALIDATION_STEPS).history

您的结果应该与图 6-4 中的结果类似。

这是如何利用tf.keras.Sequential来构建和训练深度学习模型的。正如您所看到的,只要您指定输入和输出形状与图像和标签一致,您可以堆叠任意多的层。训练过程也非常常规;它不偏离您在第五章中看到的内容。

图像分类模型架构

图 6-3. 图像分类模型架构

模型训练结果

图 6-4. 模型训练结果

在我们查看命令式 API 之前,我们将进行一个快速的绕道:您需要了解 Python 中的类继承的一些知识才能理解命令式 API。

理解继承

继承是面向对象编程中使用的一种技术。它使用的概念来封装与特定类型对象相关的属性和方法。它还处理不同类型对象之间的关系。继承是允许特定类使用另一个类中的方法的手段。

通过一个简单的例子更容易理解这个工作原理。想象我们有一个名为vehicle的基类(或父类)。我们还有另一个类truck,它是vehicle子类:这也被称为派生类继承类。我们可以定义vehicle类如下:

class vehicle():
 def __init__(self, make, model, horsepower, weight):
 self.make = make
 self.model = model
 self.horsepower = horsepower
 self.weight = weight

 def horsepower_to_weight_ratio(self, horsepower, weight):
 hp_2_weight_ratio = horsepower / weight
 return hp_2_weight_ratio

这段代码展示了定义类的常见模式。它有一个构造函数__init__,用于初始化类的属性,比如制造商、型号、马力和重量。然后有一个名为horsepower_to_weight_ratio的函数,正如您可能猜到的,它计算车辆的马力重量比(我们将其称为 HW 比)。这个函数也可以被vehicle类的任何子类访问。

现在让我们创建truck,作为vehicle的子类:

class truck(vehicle):
 def __init__(self, make, model, horsepower, weight, payload):
 super().__init__(make, model, horsepower, weight)
 self.payload = payload

 def __call__(self, horsepower, payload):
 hp_2_payload_ratio = horsepower / payload
 return hp_2_payload_ratio

在这个定义中,class truck(vehicle)表示truckvehicle的子类。

在构造函数__init__中,super返回父类vehicle的临时对象给truck类。然后这个对象调用父类的__init__,这使得truck类能够重用父类中定义的相同属性:制造商、型号、马力和重量。然而,卡车还有一个独特的属性:有效载荷。这个属性不是从基类继承的;相反,它是在truck类中定义的。您可以用self.payload = payload定义有效载荷。这里,self关键字指的是这个类的实例。在这种情况下,它是一个truck实例,而payload是您为这个属性定义的任意名称。

接下来是一个__call__函数。这个函数使truck类“可调用”。在我们探讨__call__做什么或类可调用意味着什么之前,让我们定义一些参数并创建一个truck实例:

MAKE = 'Tesla'
MODEL = 'Cybertruck'
HORSEPOWER = 800 #HP
WEIGHT = 3000 #kg
PAYLOAD = 1600 #kg

MyTruck = truck(MAKE, MODEL, HORSEPOWER, WEIGHT, PAYLOAD)

为了确保这样做得当,请打印这些属性:

print('Make: ', MyTruck.make,
 '\nModel: ', MyTruck.model,
 '\nHorsepower (HP): ', MyTruck.horsepower,
 '\nWeight (kg): ', MyTruck.weight,
 '\nPayload (kg): ', MyTruck.payload)

这应该产生以下输出:

Make: Tesla
Model: Cybertruck
Horsepower (HP): 800
Weight (kg): 3000
Payload (kg): 1600

让一个 Python 类变得可调用意味着什么?假设您是一名砌砖工,需要在卡车上运送重物。对您来说,卡车最重要的属性是其马力与有效载荷比(HP 比率)。幸运的是,您可以创建一个truck对象的实例,并立即计算比率:

MyTruck(HORSEPOWER, PAYLOAD)

输出将是 0.5。

这意味着MyTruck实例实际上有一个与之关联的值。这个值被定义为马力与有效载荷比。这个计算是由truck类的__call__函数完成的,这是 Python 类的内置函数。当这个函数被显式定义为执行某种逻辑时,它几乎像一个函数调用。再看一下这行代码:

MyTruck(HORSEPOWER, PAYLOAD)

如果您只看到这一行,您可能会认为MyTruck是一个函数,而HORSEPOWERPAYLOAD是输入。

通过显式定义__call__方法来计算 HP 比率,您使truck类可调用;换句话说,您使其表现得像一个函数。现在它可以像 Python 函数一样被调用。

接下来我们想要找到我们的对象MyTruck的 HW 比率。您可能会注意到truck类中没有为此定义任何方法。然而,由于父类vehicle中确实有这样一个方法,horsepower_to_weight_ratioMyTruck可以使用这个方法进行计算。这是类继承的演示,子类可以使用父类直接定义的方法。要做到这一点,您可以使用:

MyTruck.horsepower_to_weight_ratio(HORSEPOWER, WEIGHT)

输出是 0.26666666666666666。

使用命令式 API

看过 Python 的类继承如何工作后,您现在可以学习如何使用命令式 API 构建模型。命令式 API 也被称为模型子类 API,因为您构建的任何模型实际上都是从一个“Model”类继承的。如果您熟悉面向对象编程语言,如 C#、C++或 Java,那么命令式风格应该感觉很熟悉。

将模型定义为一个类

在前面的部分中,您如何定义您构建的模型为一个类?让我们看看代码:

class myModel(tf.keras.Model):
 def __init__(self, input_dim):
 super(myModel, self).__init__()
 self.conv2d_initial = tf.keras.layers.Conv2D(32, 
 kernel_size=(3, 3),
 activation='relu',
 kernel_initializer='glorot_uniform',
 padding='same',
 input_shape = (input_dim,input_dim,3))
 self.cov2d_mid = tf.keras.layers.Conv2D(64, kernel_size=(3, 3),
 activation='relu',
 kernel_initializer='glorot_uniform',
 padding='same')
 self.maxpool2d = tf.keras.layers.MaxPooling2D(pool_size=(2, 2))
 self.flatten = tf.keras.layers.Flatten()
 self.dense = tf.keras.layers.Dense(256, activation='relu',
 kernel_initializer='glorot_uniform')
 self.fc = tf.keras.layers.Dense(10, activation='softmax',
 name = 'custom_class')

 def call(self, input_dim):
 x = self.conv2d_initial(input_dim)
 x = self.maxpool2d(x)
 x = self.cov2d_mid(x)
 x = self.maxpool2d(x)
 x = self.flatten(x)
 x = self.dense(x)
 x = self.fc(x)

 return x

正如前面的代码所示,myModel类从父类tf.keras.Model继承,就像我们的truck类从父类vehicle继承一样。

模型中的层被视为myModel类中的属性。这些属性在构造函数__init__中定义。(回想一下,属性是参数,如马力、制造商和型号,而层是通过语法定义的,如tf.keras.layers.Conv2D。)对于模型中的第一层,代码是:

self.conv2d_initial = tf.keras.layers.Conv2D(32, 
 kernel_size=(3, 3),
 activation='relu',
 kernel_initializer='glorot_uniform',
 padding='same',
 input_shape = (input_dim,input_dim,3))

正如您所看到的,分配层只需要一个名为conv2d_initial的对象。在这个定义中的另一个重要元素是,您可以将用户定义的参数传递给属性。在这里,构造函数__init__期望用户提供一个参数input_dim,它将传递给input_shape参数。

这种风格的好处在于,如果您想要为其他类型的图像尺寸重用此模型架构,您无需创建新模型;只需将图像尺寸作为用户参数传递给此类,您将获得一个可以处理您选择的图像尺寸的类的实例。实际上,您可以向构造函数的输入添加更多用户参数,并将它们传递到对象的不同部分,比如kernel_size。这是面向对象编程风格促进代码重用的一种方式。

让我们再看一下另一个层的定义:

self.maxpool2d = tf.keras.layers.MaxPooling2D(pool_size=(2, 2))

这个层将在模型架构中多次使用,但您只需要定义一次。但是,如果您需要不同的超参数值,比如不同的pool_size,那么您需要创建另一个属性:

self.maxpool2d_2 = tf.keras.layers.MaxPooling2D(pool_size=(5, 5))

在这里,没有必要这样做,因为我们的模型架构重用了maxpool2d

现在让我们看一下call函数。回想一下,通过处理 Python 内置的__call__函数中的某些类型的逻辑或计算,您可以使一个类可调用。在类似的精神中,TensorFlow 创建了一个内置的call函数,使模型类可调用。在这个函数内部,您可以看到层的顺序与顺序 API 中的顺序相同(如您在“构建模型”中看到的)。唯一的区别是这些层现在由类属性表示,而不是硬编码的层定义。

此外,请注意,在以下输入中,用户参数input_dim被传递给属性:

def call(self, input_dim)

这可以根据您的图像尺寸要求为您的模型提供灵活性和可重用性。

call函数中,对象x被用来迭代表示模型层。在声明最终层self.fc(x)之后,它将x作为模型返回。

要创建一个处理 32×32 像素 CIFAR-10 图像尺寸的模型实例,请将实例定义为:

mdl = myModel(32)

此代码创建了一个myModel实例,并用 CIFAR-10 数据集的图像尺寸进行初始化。这个模型表示为mdl对象。接下来,就像您在“构建模型”中所做的那样,您必须使用相同的语法指定损失函数和优化器选择:

mdl.compile(loss='sparse_categorical_crossentropy',
 optimizer=tf.keras.optimizers.SGD(learning_rate=0.1, 
 momentum=0.9),
 metrics=['accuracy'])

现在您可以启动训练例程:

mdl_hist = mdl.fit(
 train_dataset,
 epochs=5, steps_per_epoch=STEPS_PER_EPOCH,
 validation_data=validation_dataset,
 validation_steps=VALIDATION_STEPS).history

您可以期望与图 6-5 中的训练结果类似的训练结果。

命令式 API 模型训练结果

图 6-5。命令式 API 模型训练结果

使用符号 API 和命令式 API 训练的模型应该产生类似的训练结果。

选择 API

您已经看到符号 API 和命令式 API 可以用来构建具有相同架构的模型。在大多数情况下,您选择 API 的依据将基于您喜欢的风格和对语法的熟悉程度。然而,值得注意的是有一些值得注意的权衡。

符号 API 的最大优势是其代码可读性,这使得维护更容易。可以直观地看到模型架构,并且可以看到输入数据通过不同层的张量流动,就像一个图一样。使用符号 API 构建的模型还可以利用tf.keras.utils.plot_model来显示模型架构。通常,这是我们设计深度学习模型时的起点。

当涉及到实现模型架构时,命令式 API 绝对不像符号 API 那样直接。正如您所了解的,这种风格源自类继承的面向对象编程技术。如果您更喜欢将模型视为一个对象而不是一堆操作层,您可能会发现这种风格更直观,如图 6-6 所示。

TensorFlow 模型的命令式 API(也称为模型子类化)

图 6-6。TensorFlow 模型的命令式 API(也称为模型子类化)

实质上,您构建的任何模型都是基本模型tf.keras.Model扩展或继承类。因此,当您构建一个模型时,实际上只是创建了一个继承了基本模型所有属性和函数的类的实例。要适应不同维度的图像模型,您只需使用不同的超参数实例化它。如果重用相同的模型架构是您的工作流程的一部分,那么命令式 API 是保持代码清洁简洁的明智选择。

使用内置训练循环

到目前为止,您已经看到启动模型训练过程所需的只是fit函数。这个函数为您包装了许多复杂的操作,如图 6-7 所示。

内置训练循环中的要素

图 6-7。内置训练循环中的要素

模型对象包含有关架构、损失函数、优化器和模型指标的信息。在fit中,您提供训练和验证数据,要训练的时期数,以及多久更新模型参数并使用验证数据进行测试。

这就是您需要做的全部。内置训练循环知道当一个训练时期完成时,是时候使用批处理验证数据执行交叉验证了。这很方便清晰,使您的代码非常易于维护。输出在每个时期结束时产生,如图 6-4 和 6-5 所示。

如果您需要查看训练过程的详细信息,例如在时期结束之前每个增量改进步骤中的模型准确性,或者如果您想要创建自己的训练指标,那么您需要构建自己的训练循环。接下来,我们将看看这是如何工作的。

创建和使用自定义训练循环

使用自定义训练循环,您失去了fit函数的便利性;相反,您需要编写代码来编排训练过程。假设您想要在每个步骤中监视模型参数在一个时期内的准确性。您可以从“构建模型”中重用模型对象(model)。

创建循环的要素

首先,创建优化器和损失函数对象:

optimizer = tf.keras.optimizers.SGD(
learning_rate=0.1, 
 momentum=0.9)
loss_fn = tf.keras.losses.SparseCategoricalCrossentropy(
from_logits=True)

然后创建代表模型指标的对象:

train_acc_metric = tf.keras.metrics.SparseCategoricalAccuracy()
val_acc_metric = tf.keras.metrics.SparseCategoricalAccuracy()

这段代码为模型准确性创建了两个对象:一个用于训练数据,一个用于验证数据。使用SparseCategoricalAccuracy函数是因为输出是一个计算预测与标签匹配频率的指标。

接下来,对于训练,您需要创建一个函数:

@tf.function
def train_step(train_data, train_label):
    with tf.GradientTape() as tape:
    logits = model(train_data, training=True)
    loss_value = loss_fn(train_label, logits)
    grads = tape.gradient(loss_value, model.trainable_weights)
    optimizer.apply_gradients(zip(grads, model.trainable_weights))
    train_acc_metric.update_state(train_label, logits)
    return loss_value

在前面的代码中,@tf.function是一个 Python 装饰器,它将一个以张量作为输入的函数转换为一个可以加速函数执行的形式。这个函数还包括一个新对象tf.GradientTape。在这个范围内,TensorFlow 为您执行梯度下降算法;它通过不同 iating 每个节点中的训练权重相对于损失函数的梯度来自动计算梯度。

以下行指示GradientTape对象的范围:

with tf.GradientTape() as tape

接下来的代码行表示您调用model将训练数据映射到一个输出(logits):

logits = model(train_data, training=True)

现在计算损失函数的输出,将模型输出与真实标签train_label进行比较:

loss_value = loss_fn(train_label, logits)

然后使用模型的参数(trainable_weights)和损失函数的值(loss_value)来计算梯度并更新模型的准确性。

您需要对验证数据执行相同的操作:

@tf.function
def test_step(validation_data, validation_label):
 val_logits = model(validation_data, training=False)
 val_acc_metric.update_state(validation_label, val_logits)

将要素组合在一起形成自定义训练循环

现在您已经拥有所有的要素,可以开始创建自定义训练循环了。以下是一般的步骤:

  1. 使用for循环来迭代每个时期。

  2. 在每个时期内,使用另一个for循环来迭代数据集中的每个批次。

  3. 在每个批次中,打开一个GradientTape对象范围。

  4. 在范围内,计算损失函数。

  5. 在范围外,检索模型权重的梯度。

  6. 使用优化器根据梯度值更新模型权重。

以下是自定义训练循环的代码片段:

import time

epochs = 2
for epoch in range(epochs):
 print("\nStarting epoch %d" % (epoch,))
 start_time = time.time()

 # Iterate dataset batches
 for step, (x_batch_train, y_batch_train) in 
 enumerate(train_dataset):
 loss_value = train_step(x_batch_train, y_batch_train)

    # In every 100 batches, log results.
    if step % 100 == 0:
         print(
         "Training loss (for one batch) at step %d: %.4f"
         % (step, float(loss_value))
         )
 print("Sample processed so far: %d samples" % 
 ((step + 1) * TRAIN_BATCH_SIZE))

 # Show accuracy metrics after each epoch is completed
 train_accuracy = train_acc_metric.result()
 print("Training accuracy over epoch: %.4f" % 
 (float(train_accuracy),))

 # Reset training metrics before next epoch starts
 train_acc_metric.reset_states()

 # Test with validation data at end of each epoch
 for x_batch_val, y_batch_val in validation_dataset:
 test_step(x_batch_val, y_batch_val)

 val_accuracy = val_acc_metric.result()
 val_acc_metric.reset_states()
 print("Validation accuracy: %.4f" % (float(val_accuracy),))
 print("Time taken: %.2fs" % (time.time() - start_time))

图 6-8 显示了执行自定义训练循环的典型输出。

执行自定义训练循环的输出

图 6-8。执行自定义训练循环的输出

正如您所看到的,每个 200 个样本批次结束时,训练循环会计算并显示损失函数的值,让您可以查看训练过程内部发生的情况。如果您需要这种可见性,构建自己的自定义训练循环将提供它。只需知道,这比fit函数的便捷内置训练循环需要更多的努力。

总结

在本章中,您学习了如何使用符号和命令式 API 在 TensorFlow 中构建深度学习模型。通常情况下,两者都能够实现相同的架构,特别是当数据从输入到输出以直线流动时(意味着没有反馈或多个输入)。您可能会看到使用命令式 API 的复杂架构和定制实现的模型。选择适合您情况、方便和可读性的 API。

无论您选择哪种方式,您都将使用内置的fit函数以相同的方式训练模型。fit函数执行内置的训练循环,并让您不必担心如何实际编排训练过程。诸如计算损失函数、将模型输出与真实标签进行比较以及使用梯度值更新模型参数等细节都在幕后为您处理。您将看到的是每个时代结束时的结果:模型相对于训练数据和交叉验证数据的准确性。

如果您需要查看每个批次训练数据中模型的准确性等时代内部发生的情况,那么您需要编写自己的训练循环,这是一个相当费力的过程。

在下一章中,您将看到模型训练过程中提供的其他选项,这些选项提供了更多的灵活性,而无需进行自定义训练循环的复杂编码过程。

第七章:监控训练过程

在上一章中,您学习了如何启动模型训练过程。在本章中,我们将介绍过程本身。

在本书中,我使用了相当直接的例子来帮助您理解每个概念。然而,在 TensorFlow 中运行真实的训练过程时,事情可能会更加复杂。例如,当出现问题时,您需要考虑如何确定您的模型是否过拟合训练数据(当模型学习并记忆训练数据和训练数据中的噪声以至于负面影响其学习新数据时就会发生过拟合)。如果是,您需要设置交叉验证。如果不是,您可以采取措施防止过拟合。

在训练过程中经常出现的其他问题包括:

  • 在训练过程中应该多久保存一次模型?

  • 在过拟合发生之前,我应该如何确定哪个时期给出了最佳模型?

  • 我如何跟踪模型性能?

  • 如果模型没有改进或出现过拟合,我可以停止训练吗?

  • 有没有一种方法可以可视化模型训练过程?

TensorFlow 提供了一种非常简单的方法来解决这些问题:回调函数。在本章中,您将学习如何快速使用回调函数来监视训练过程。本章的前半部分讨论了ModelCheckpointEarlyStopping,而后半部分侧重于 TensorBoard,并向您展示了几种调用 TensorBoard 和使用它进行可视化的技巧。

回调对象

TensorFlow 的回调对象是一个可以执行由tf.keras提供的一组内置函数的对象。当训练过程中发生某些事件时,回调对象将执行特定的代码或函数。

使用回调是可选的,因此您不需要实现任何回调对象来训练模型。我们将看一下最常用的三个类:ModelCheckpointEarlyStopping和 TensorBoard。¹

ModelCheckpoint

ModelCheckpoint类使您能够在训练过程中定期保存模型。默认情况下,在每个训练时期结束时,模型的权重和偏差会被最终确定并保存为权重文件。通常,当您启动训练过程时,模型会从该时期的训练数据中学习并更新权重和偏差,这些权重和偏差会保存在您在开始训练过程之前指定的目录中。然而,有时您只想在模型从上一个时期改进时保存模型,以便最后保存的模型始终是最佳模型。为此,您可以使用ModelCheckpoint类。在本节中,您将看到如何在模型训练过程中利用这个类。

让我们尝试在第六章中使用的 CIFAR-10 图像分类数据集中进行。像往常一样,我们首先导入必要的库,然后读取 CIFAR-10 数据:

import tensorflow as tf
from tensorflow.keras import datasets, layers, models
import numpy as np
import matplotlib.pylab as plt
import os
from datetime import datetime

(train_images, train_labels), (test_images, test_labels) = 
datasets.cifar10.load_data()

首先,将图像中的像素值归一化为 0 到 1 的范围:

train_images, test_images = train_images / 255.0, 
test_images / 255.0

该数据集中的图像标签由整数组成。使用 NumPy 命令验证这一点:

np.unique(train_labels)

这显示的值为:

array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype=uint8)

现在您可以将这些整数映射到纯文本标签。这里提供的标签(由 Alex Krizhevsky、Vinod Nair 和 Geoffrey Hinton 提供)按字母顺序排列。因此,airplanetrain_labels中映射为 0,而truck映射为 9:

CLASS_NAMES = ['airplane', 'automobile', 'bird', 'cat',
               'deer', 'dog', 'frog', 'horse', 'ship', 'truck']

由于test_images有一个单独的分区,从test_images中提取前 500 张图像用于交叉验证,并将其命名为validation_images。剩下的图像将用于测试。

为了更有效地利用计算资源,将test_images的图像和标签从其原生 NumPy 数组格式转换为数据集格式:

validation_dataset = tf.data.Dataset.from_tensor_slices(
(test_images[:500], 
 test_labels[:500]))

test_dataset = tf.data.Dataset.from_tensor_slices(
(test_images[500:], 
 test_labels[500:]))

train_dataset = tf.data.Dataset.from_tensor_slices(
(train_images,
 train_labels))

执行这些命令后,您应该在三个数据集中拥有所有图像:一个训练数据集(train_dataset)、一个验证数据集(validation_dataset)和一个测试数据集(test_dataset)。

知道这些数据集的大小会很有帮助。要找到 TensorFlow 数据集的样本大小,将其转换为列表,然后使用len函数找到列表的长度:

train_dataset_size = len(list(train_dataset.as_numpy_iterator()))
print('Training data sample size: ', train_dataset_size)

validation_dataset_size = len(list(validation_dataset.
as_numpy_iterator()))
print('Validation data sample size: ', 
validation_dataset_size)

test_dataset_size = len(list(test_dataset.as_numpy_iterator()))
print('Test data sample size: ', test_dataset_size)

您可以期待以下结果:

Training data sample size:  50000
Validation data sample size:  500
Test data sample size:  9500

接下来,对这三个数据集进行洗牌和分批处理:

TRAIN_BATCH_SIZE = 128
train_dataset = train_dataset.shuffle(50000).batch(
TRAIN_BATCH_SIZE, 
drop_remainder=True)

validation_dataset = validation_dataset.batch(
 validation_dataset_size)
test_dataset = test_dataset.batch(test_dataset_size)

请注意,train_dataset将被分成多个批次。每个批次将包含TRAIN_BATCH_SIZE个样本(在本例中为 128)。每个训练批次在训练过程中被馈送到模型中,以实现对权重和偏差的增量更新。对于验证和测试,不需要创建多个批次。它们将作为一个批次使用,但仅用于记录指标和测试。

接下来,指定多久更新权重和验证一次:

STEPS_PER_EPOCH = train_dataset_size // TRAIN_BATCH_SIZE
VALIDATION_STEPS = 1

前面的代码意味着在模型看到由STEPS_PER_EPOCH指定的训练数据批次数量后,是时候使用验证数据集(作为一个批次)测试模型了。

为此,您首先要定义模型架构:

model = tf.keras.Sequential([
    tf.keras.layers.Conv2D(32, kernel_size=(3, 3), 
      activation='relu',
      kernel_initializer='glorot_uniform', padding='same', 
      input_shape = (32,32,3)),
    tf.keras.layers.MaxPooling2D(pool_size=(2, 2)),
    tf.keras.layers.Conv2D(64, kernel_size=(3, 3), 
     activation='relu',
      kernel_initializer='glorot_uniform', padding='same'),
    tf.keras.layers.MaxPooling2D(pool_size=(2, 2)),
    tf.keras.layers.Flatten(),
    tf.keras.layers.Dense(256, 
     activation='relu', kernel_initializer='glorot_uniform'),
    tf.keras.layers.Dense(10, activation='softmax', 
    name = 'custom_class')
])
model.build([None, 32, 32, 3])

现在,编译模型以确保它设置正确:

model.compile(
          loss=tf.keras.losses.SparseCategoricalCrossentropy(
               from_logits=True),
          optimizer='adam',
          metrics=['accuracy'])

接下来,命名 TensorFlow 应在每个检查点保存模型的文件夹。通常,您会多次重新运行训练例程,可能会觉得每次创建一个唯一的文件夹名称很烦琐。一个简单且经常使用的方法是在模型名称后附加一个时间戳:

MODEL_NAME = 'myCIFAR10-{}'.format(datetime.now().strftime(
"%Y%m%d-%H%M%S"))
print(MODEL_NAME)

前面的命令会产生一个名为myCIFAR10-20210123-212138的名称。您可以将此名称用于检查点目录:

checkpoint_dir = './' + MODEL_NAME
checkpoint_prefix = os.path.join(checkpoint_dir, "ckpt-{epoch}")
print(checkpoint_prefix)

前面的命令指定了目录路径为*./myCIFAR10-20210123-212138/ckpt-{epoch}。该目录位于当前目录的下一级。{epoch}*将在训练期间用 epoch 号进行编码。现在定义myCheckPoint对象:

myCheckPoint = tf.keras.callbacks.ModelCheckpoint(
    filepath=checkpoint_prefix,
    monitor='val_accuracy',
    mode='max')

在这里,您指定了 TensorFlow 将在每个 epoch 保存模型的文件路径。您还设置了监视验证准确性。

当您使用回调启动训练过程时,回调将期望一个 Python 列表。因此,让我们将myCheckPoint对象放入 Python 列表中:

myCallbacks = [
    myCheckPoint
]

现在启动训练过程。此命令将整个模型训练历史分配给对象hist,这是一个 Python 字典:

hist = model.fit(
    train_dataset,
    epochs=12,
    steps_per_epoch=STEPS_PER_EPOCH,
    validation_data=validation_dataset,
    validation_steps=VALIDATION_STEPS,
    callbacks=myCallbacks).history

您可以使用命令hist['val_accuracy']查看从训练的第一个 epoch 到最后一个 epoch 的交叉验证准确性。显示应该类似于这样:

[0.47200000286102295,
 0.5680000185966492,
 0.6000000238418579,
 0.5899999737739563,
 0.6119999885559082,
 0.6019999980926514,
 0.6100000143051147,
 0.6380000114440918,
 0.6100000143051147,
 0.5699999928474426,
 0.5619999766349792,
 0.5960000157356262]

在这种情况下,交叉验证准确性在一些 epoch 中有所提高,然后逐渐下降。这种下降是过拟合的典型迹象。这里最好的模型是具有最高验证准确性(数组中最高值)的模型。要确定其在数组中的位置(或索引),请使用此代码:

max_value = max(hist['val_accuracy'])
max_index = hist['val_accuracy'].index(max_value)
print('Best epoch: ', max_index + 1)

请记住将max_index加 1,因为 epoch 从 1 开始,而不是 0(与 NumPy 数组索引不同)。输出是:

Best epoch:  8

接下来,通过在 Jupyter Notebook 单元格中运行以下 Linux 命令来查看检查点目录:

!ls -lrt ./cifar10_training_checkpoints

您将看到此目录的内容(如图 7-1 所示)。

在每个检查点保存的模型

图 7-1。在每个检查点保存的模型

您可以重新运行此命令并指定特定目录,以查看特定 epoch 构建的模型(如图 7-2 所示):

!ls -lrt ./cifar10_training_checkpoints/ckpt_8

在检查点 8 保存的模型文件

图 7-2。在检查点 8 保存的模型文件

到目前为止,您已经看到如何使用CheckPoint在每个 epoch 保存模型。如果您只希望保存最佳模型,请指定save_best_only = True

best_only_checkpoint_dir = 
 './best_only_cifar10_training_checkpoints'
best_only_checkpoint_prefix = os.path.join(
best_only_checkpoint_dir, 
"ckpt_{epoch}")

bestCheckPoint = tf.keras.callbacks.ModelCheckpoint(
    filepath=best_only_checkpoint_prefix,
    monitor='val_accuracy',
    mode='max',
    save_best_only=True)

然后将bestCheckPoint放入回调列表中:

    bestCallbacks = [
    bestCheckPoint
]

之后,您可以启动训练过程:

best_hist = model.fit(
    train_dataset,
    epochs=12,
    steps_per_epoch=STEPS_PER_EPOCH,
    validation_data=validation_dataset,
    validation_steps=VALIDATION_STEPS,
    callbacks=bestCallbacks).history

在这个训练中,而不是保存所有检查点,bestCallbacks只有在验证准确率比上一个 epoch 更好时才保存模型。save_best_only选项允许您在第一个 epoch 之后在模型指标有增量改进时保存检查点(使用monitor指定),因此最后保存的检查点是最佳模型。

要查看您保存的内容,请在 Jupyter Notebook 单元格中运行以下命令:

 !ls -lrt ./best_only_cifar10_training_checkpoints

在图 7-3 中显示了验证准确率逐渐提高的保存模型。

设置 save_best_only 为 True 保存的模型

图 7-3。设置 save_best_only 为 True 的模型

第一个 epoch 的模型始终会被保存。在第三个 epoch,模型在验证准确率上有所改进,因此第三个检查点模型被保存。训练继续进行。第九个 epoch 中验证准确率提高,因此第九个检查点模型是最后一个被保存的目录。训练持续到第 12 个 epoch,没有进一步增加验证准确率的改进。这意味着第九个检查点目录包含了最佳验证准确率的模型。

现在您熟悉了ModelCheckpoint,让我们来看看另一个回调对象:EarlyStopping

EarlyStopping

EarlyStopping回调对象使您能够在达到最终 epoch 之前停止训练过程。通常,如果模型没有改进,您会这样做以节省训练时间。

该对象允许您指定一个模型指标,例如验证准确率,通过所有 epoch 进行监视。如果指定的指标在一定数量的 epoch 后没有改进,训练将停止。

要定义一个EarlyStopping对象,请使用以下命令:

myEarlyStop = tf.keras.callbacks.EarlyStopping(
monitor='val_accuracy',
patience=4)

在这种情况下,您在每个 epoch 监视验证准确率。您将patience参数设置为 4,这意味着如果验证准确率在四个 epoch 内没有改进,训练将停止。

提示

要了解更多自定义提前停止的方法,请参阅TensorFlow 2 文档

要在回调中使用ModelCheckpoint对象实现提前停止,需要将其放入列表中:

myCallbacks = [
    myCheckPoint,
    myEarlyStop
]

训练过程是相同的,但您指定了callbacks=myCallbacks

hist = model.fit(
    train_dataset,
    epochs=20,
    steps_per_epoch=STEPS_PER_EPOCH,
    validation_data=validation_dataset,
    validation_steps=VALIDATION_STEPS,
    callbacks=myCallbacks).history

一旦您启动了前面的训练命令,输出应该类似于图 7-4。

训练过程中的提前停止

图 7-4。训练过程中的提前停止

在图 7-4 中显示的训练中,最佳验证准确率出现在第 15 个 epoch,值为 0.7220。再经过四个 epoch,验证准确率没有超过该值,因此在第 19 个 epoch 后停止训练。

总结

ModelCheckpoint类允许您设置条件或频率以在训练期间保存模型,而EarlyStopping类允许您在模型没有改进到您选择的指标时提前终止训练。这些类一起在 Python 列表中指定,并将此列表作为回调传递到训练例程中。

许多其他用于监控训练进度的功能可用(请参阅tf.keras.callbacks.CallbackKeras Callbacks API),但ModelCheckpointEarlyStopping是最常用的两个。

本章的其余部分将深入探讨被称为TensorBoard的流行回调类,它提供了您的训练进度和结果的可视化表示。

TensorBoard

如果您希望可视化您的模型和训练过程,TensorBoard 是您的工具。TensorBoard 提供了一个视觉表示,展示了您的模型参数和指标在训练过程中如何演变。它经常用于跟踪训练周期中的模型准确性。它还可以让您看到每个模型层中的权重和偏差如何演变。就像ModelCheckpointEarlyStopping一样,TensorBoard 通过回调模块应用于训练过程。您创建一个代表Tensorboard的对象,然后将该对象作为回调列表的成员传递。

让我们尝试构建一个对 CIFAR-10 图像进行分类的模型。像往常一样,首先导入库,加载 CIFAR-10 图像,并将像素值归一化到 0 到 1 的范围内:

import tensorflow as tf
from tensorflow.keras import datasets, layers, models
import numpy as np
import matplotlib.pylab as plt
import os
from datetime import datetime

(train_images, train_labels), (test_images, test_labels) = 
datasets.cifar10.load_data()

train_images, test_images = train_images / 255.0, 
 test_images / 255.0

定义您的纯文本标签:

CLASS_NAMES = ['airplane', 'automobile', 'bird', 'cat',
               'deer','dog', 'frog', 'horse', 'ship', 'truck']

现在将图像转换为数据集:

validation_dataset = tf.data.Dataset.from_tensor_slices(
(test_images[:500], test_labels[:500]))

test_dataset = tf.data.Dataset.from_tensor_slices(
(test_images[500:], test_labels[500:]))

train_dataset = tf.data.Dataset.from_tensor_slices(
(train_images, train_labels))

然后确定用于训练、验证和测试分区的数据大小:

train_dataset_size = len(list(train_dataset.as_numpy_iterator()))
print('Training data sample size: ', train_dataset_size)

validation_dataset_size = len(list(validation_dataset.
as_numpy_iterator()))
print('Validation data sample size: ', 
 validation_dataset_size)

test_dataset_size = len(list(test_dataset.as_numpy_iterator()))
print('Test data sample size: ', test_dataset_size)

您的结果应该如下所示:

Training data sample size:  50000
Validation data sample size:  500
Test data sample size:  9500

现在您可以对数据进行洗牌和分批处理:

TRAIN_BATCH_SIZE = 128
train_dataset = train_dataset.shuffle(50000).batch(
TRAIN_BATCH_SIZE, 
drop_remainder=True)

validation_dataset = validation_dataset.batch(
validation_dataset_size)
test_dataset = test_dataset.batch(test_dataset_size)

然后指定参数以设置更新模型权重的节奏:

STEPS_PER_EPOCH = train_dataset_size // TRAIN_BATCH_SIZE
VALIDATION_STEPS = 1

STEPS_PER_EPOCH是一个整数,从train_dataset_sizeTRAIN_BATCH_SIZE之间的除法向下取整得到。(双斜杠表示除法并向下取整到最接近的整数。)

我们将重用我们在“ModelCheckpoint”中构建的模型架构:

model = tf.keras.Sequential([
    tf.keras.layers.Conv2D(32, 
     kernel_size=(3, 3), 
     activation='relu', 
     name = 'conv_1',
     kernel_initializer='glorot_uniform', 
     padding='same', input_shape = (32,32,3)),
    tf.keras.layers.MaxPooling2D(pool_size=(2, 2)),
    tf.keras.layers.Conv2D(64, kernel_size=(3, 3), 
     activation='relu', name = 'conv_2',
      kernel_initializer='glorot_uniform', padding='same'),
    tf.keras.layers.MaxPooling2D(pool_size=(2, 2)),
    tf.keras.layers.Conv2D(64, 
     kernel_size=(3, 3), 
     activation='relu', 
     name = 'conv_3',
      kernel_initializer='glorot_uniform', padding='same'),
    tf.keras.layers.Flatten(name = 'flat_1',),
    tf.keras.layers.Dense(64, activation='relu',   
     kernel_initializer='glorot_uniform', 
     name = 'dense_64'),
    tf.keras.layers.Dense(10, 
     activation='softmax', 
     name = 'custom_class')
])
model.build([None, 32, 32, 3])

请注意,这次每个层都有一个名称。为每个层指定一个名称有助于您知道您正在检查哪个层。这不是必需的,但对于在 TensorBoard 中进行可视化是一个好的实践。

现在编译模型以确保模型架构有效,并指定损失函数:

model.compile(
          loss=tf.keras.losses.SparseCategoricalCrossentropy(
               from_logits=True),
          optimizer='adam',
          metrics=['accuracy'])

设置模型名称将在以后有所帮助,当 TensorBoard 让您选择一个模型(或多个模型)并检查其训练结果的可视化时。您可以像在使用ModelCheckpoint时那样在模型名称后附加一个时间戳:

MODEL_NAME =
'myCIFAR10-{}'.format(datetime.now().strftime("%Y%m%d-%H%M%S"))

print(MODEL_NAME)

在这个例子中,MODEL_NAMEmyCIFAR10-20210124-135804。您的将类似。

接下来,设置检查点目录:

checkpoint_dir = './' + MODEL_NAME
checkpoint_prefix = os.path.join(checkpoint_dir, "ckpt-{epoch}")
print(checkpoint_prefix)

*./myCIFAR10-20210124-135804/ckpt-{epoch}*是这个检查点目录的名称。

定义模型检查点:

myCheckPoint = tf.keras.callbacks.ModelCheckpoint(
    filepath=checkpoint_prefix,
    monitor='val_accuracy',
mode='max')

接下来,您将定义一个 TensorBoard,然后我们将更仔细地查看这段代码:

myTensorBoard = tf.keras.callbacks.TensorBoard(
log_dir='./tensorboardlogs/{}'.format(MODEL_NAME),
write_graph=True,
write_images=True,
histogram_freq=1)

这里指定的第一个参数是log_dir。这是您想要保存训练日志的路径。如指示,它在当前级别下的一个名为tensorboardlogs的目录中,后面跟着一个名为MODEL_NAME的子目录。随着训练的进行,您的日志将在这里生成和存储,以便 TensorBoard 可以解析它们进行可视化。

参数write_graph设置为 True,这样模型图将被可视化。另一个参数write_images也设置为 True。这确保模型权重将被写入日志,这样您可以可视化它们在训练过程中的变化。

最后,histogram_freq设置为 1。这告诉 TensorBoard 何时按 epoch 创建可视化:1 表示每个 epoch 创建一个可视化。有关更多参数,请参阅 TensorBoard 的文档

最后,您有两个回调对象要设置:myCheckPointmyTensorBoard。要将两者放入 Python 列表中,您只需执行以下操作:

myCallbacks = [
    myCheckPoint,
    myTensorBoard
]

然后将您的myCallbacks列表传递到训练例程中:

hist = model.fit(
    train_dataset,
    epochs=30,
    steps_per_epoch=STEPS_PER_EPOCH,
    validation_data=validation_dataset,
    validation_steps=VALIDATION_STEPS,
    callbacks=myCallbacks).history

一旦训练过程完成,有三种方法可以调用 TensorBoard。您可以在您自己的计算机上的 Jupyter Notebook 中的下一个单元格中运行它,也可以在您自己计算机上的命令终端中运行,或者在 Google Colab 中运行。我们将依次查看这些选项。

通过本地 Jupyter Notebook 调用 TensorBoard

如果您选择使用您的 Jupyter Notebook,在下一个单元格中运行以下命令:

!tensorboard --logdir='./tensorboardlogs/'

请注意,在这种情况下,当您指定路径以查找训练日志时,参数是logdir,而不是在定义myTensorBoard时的log_dir

运行上述命令后,您将看到以下内容:

Serving TensorBoard on localhost; to expose to the network, use a
proxy or pass --bind_all
TensorBoard 2.3.0 at http://localhost:6006/ (Press CTRL+C to quit)

正如您所看到的,TensorBoard 正在您当前的计算实例(localhost)上以端口号 6006 运行。

现在打开浏览器,导航至*http://localhost:6006*,您将看到 TensorBoard 正在运行,如图 7-5 所示。

TensorBoard 可视化

图 7-5. TensorBoard 可视化

正如您所看到的,通过每个时代,准确性和损失都在图表中追踪。训练数据显示为浅灰色,验证数据显示为深灰色。

使用 Jupyter Notebook 单元格的主要优势是方便。缺点是运行!tensorboard命令的单元格将保持活动状态,直到您停止 TensorBoard,您将无法使用此笔记本。

通过本地命令终端调用 TensorBoard

您的第二个选项是在本地环境的命令终端中启动 TensorBoard。如图 7-6 所示,等效命令是:

tensorboard --logdir='./tensorboardlogs/'

从命令终端调用 TensorBoard

图 7-6. 从命令终端调用 TensorBoard

请记住,logdir是由训练回调 API 创建的训练日志的目录路径。前面代码中的命令使用相对路径表示法;如果您愿意,可以使用完整路径。

输出与在 Jupyter Notebook 中看到的完全相同:URL(*http://localhost:6006*)。使用浏览器打开此 URL 以显示 TensorBoard。

通过 Colab 笔记本调用 TensorBoard

现在是我们的第三个选项。如果您在此练习中使用 Google Colab 笔记本,那么调用 TensorBoard 将与您迄今为止看到的有所不同。您将无法在本地计算机上打开浏览器指向 Colab 笔记本,因为它将在 Google 的云环境中运行。因此,您需要安装 TensorBoard 笔记本扩展。这可以在第一个单元格中完成,当您导入所有库时。只需添加此命令并在第一个 Colab 单元格中运行:

%load_ext tensorboard

完成后,每当您准备调用 TensorBoard(例如在训练完成后),请使用此命令:

%tensorboard --logdir ./tensorboardlogs/

您将看到输出在您的 Colab 笔记本中运行,看起来如图 7-5 所示。

使用 TensorBoard 可视化模型过拟合

当您将 TensorBoard 用作模型训练的回调时,您将获得从第一个时代到最后一个时代的模型准确性和损失的图表。

例如,对于我们的 CIFAR-10 图像分类模型,您将看到类似于图 7-5 中所示的输出。在该特定训练运行中,尽管训练和验证准确性都在提高,损失在减少,但这两个趋势开始趋于平缓,表明进一步的训练时期可能只会带来较小的收益。

还要注意,在此运行中,验证准确性低于训练准确性,而验证损失高于训练损失。这是有道理的,因为模型在训练数据上的表现比在交叉验证数据上测试时更好。

您还可以在 TensorBoard 的 Scalars 选项卡中获得图表,就像图 7-7 中所示的那样。

在图 7-7 中,较暗的线表示验证指标,而较浅灰色的线表示训练指标。训练数据中的模型准确性远高于交叉验证数据,而训练数据中的损失远低于交叉验证数据。

您可能还注意到,交叉验证准确率在第 10 个时期达到峰值,略高于 0.7。之后,验证数据准确率开始下降,而损失开始增加。这是模型过拟合的明显迹象。这些图表告诉您的是,在第 10 个时期之后,您的模型开始记忆训练数据的模式。当遇到新的、以前未见过的数据(如交叉验证图像)时,这并没有帮助。事实上,模型在交叉验证中的表现(准确率和损失)将开始变差。

在 TensorBoard 中显示的模型过拟合

图 7-7. 在 TensorBoard 中显示的模型过拟合

一旦您检查了这些图表,您将知道哪个时期提供了训练过程中最佳的模型。您还将了解模型何时开始过拟合并记忆其训练数据。

如果您的模型仍有改进的空间,就像 图 7-5 中的那个,您可能决定增加训练时期,并在过拟合模式开始出现之前继续寻找最佳模型(参见 图 7-7)。

使用 TensorBoard 可视化学习过程

TensorBoard 中的另一个很酷的功能是权重和偏差分布的直方图。这些显示为训练结果的每个时期。通过可视化这些参数是如何分布的,以及它们的分布随时间如何变化,您可以深入了解训练过程的影响。

让我们看看如何使用 TensorBoard 来检查模型的权重和偏差分布。这些信息将在 TensorBoard 的直方图选项卡中(图 7-8)。

TensorBoard 中的权重和偏差直方图

图 7-8. TensorBoard 中的权重和偏差直方图

左侧是训练过的所有模型的面板。请注意有两个模型被选中。右侧是它们的权重(表示为 kernel_0)和偏差在每个训练时期的分布。每一行的图表示模型中的特定层。第一层被命名为 conv_1,这是您在设置模型架构时给这一层取的名字。

让我们更仔细地检查这些图表。我们将从 conv_1 层开始,如 图 7-9 所示。

conv_1 层中的偏差分布经过训练

图 7-9. conv_1 层中的偏差分布经过训练

在两个模型中,conv_1 层中偏差值的分布从第一个时期(背景)到最后一个时期(前景)肯定发生了变化。方框表明随着训练的进行,这一层的所有节点中开始出现某种偏差分布模式。新值远离零,或整体分布的中心。

让我们也看一看权重的分布。这次,让我们只关注一个模型和一个层:conv_3。这在 图 7-10 中显示。

conv_3 层中的权重分布经过训练

图 7-10. conv_3 层中的权重分布经过训练

值得注意的是,随着训练的进行,分布变得更广泛、更平坦。这可以从直方图从第一个到最后一个时期的峰值计数中看出,从 1.22e+4 到 7.0e+3。这意味着直方图逐渐变得更广泛,有更多的权重被更新为远离零的值(直方图的中心)。

使用 TensorBoard,您可以检查不同层和模型训练运行的组合,看看它们如何受训练过程或模型架构的变化影响。这就是为什么 TensorBoard 经常用于直观检查模型训练过程。

总结

在本章中,您看到了一些用于跟踪模型训练过程的最流行方法。本章介绍了模型检查点的概念,并提供了两种重要的方法来帮助您管理如何在训练过程中保存模型:在每个周期保存模型,或者仅在模型指标有增量改进时保存模型。您还了解到,交叉验证中的模型准确度决定了模型何时开始过拟合训练数据。

在本章中,您了解到的另一个重要工具是 TensorBoard,它可以用来可视化训练过程。TensorBoard 通过训练周期展示基本指标(准确度和损失)的趋势的可视图像。它还允许您检查每个层的权重和偏差分布。所有这些技术都可以通过回调函数轻松实现在训练过程中。

在下一章中,您将看到如何在 TensorFlow 中实现分布式训练,利用诸如 GPU 之类的高性能计算单元,以提供更短的训练时间。

¹ 这里没有涵盖的另外两个常见且有用的功能是LearningRateSchedulerCSVLogger