六、自编码器,变分自编码器和生成对抗网络
本章将介绍一种与到目前为止所看到的模型稍有不同的模型。 到目前为止提供的所有模型都属于一种称为判别模型的模型。 判别模型旨在找到不同类别之间的界限。 他们对找到P(Y|X)
-给定某些输入X
的输出Y
的概率感兴趣。 这是用于分类的自然概率分布,因为您通常要在给定一些输入X
的情况下找到标签Y
。
但是,还有另一种类型的模型称为生成模型。 建立了生成模型以对不同类的分布进行建模。 他们对找到P(Y,X)
-输出Y
和输入X
一起出现的概率分布感兴趣。 从理论上讲,如果您可以捕获数据中类别的概率分布,则将了解更多信息,并且可以使用贝叶斯规则来计算P(Y|X)
。
生成模型属于无监督学习算法的类别。 无监督意味着我们不需要标签数据。
本章列出了一些我们将要学习的关键主题:
- 自编码器
- 变分自编码器
- 生成对抗网络
- 实现各种生成模型来生成手写数字
为什么是生成模型
在下图中,我们可以看到生成模型和判别模型之间的主要区别。 使用判别模型,我们通常尝试找到在数据中不同类别之间进行区分或“区分”的方法。 但是,使用生成模型,我们尝试找出数据的概率分布。 在图示中,分布由包含较小圆圈的蓝色和黄色大斑点表示。 如果我们从数据中学到这种分布,我们将能够采样或“生成”应该属于它的新数据点,例如红色三角形。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OXPtb5Wl-1681568428339)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/b588c84d-43c3-4d91-a8b4-9f39f4608318.png)]
尝试捕获数据集的概率分布具有以下用例:
- 使用未标记的数据预训练模型
- 扩展数据集(理论上,如果您捕获数据的概率分布,则可以生成更多数据)
- 压缩数据(有损)
- 创建某种模拟器(例如,可以通过四个输入来控制四轴飞行器;如果捕获此数据并在其上训练生成模型,则可以学习无人机的动态)
使用生成模型时的期望是,如果我们能够创建类似于原始输入数据的新数据,则我们的模型必须了解一些有关数据分布的知识。
训练了生成神经网络模型,以产生类似于训练集的数据样本。 由于模型参数的数量小于训练数据的维数,因此迫使模型发现有效的数据表示形式。
自编码器
我们将要看到的第一个生成模型是自编码器模型。 自编码器是一个简单的神经网络,由两部分组成:编码器和解码器。 这个想法是编码器部分会将您的输入压缩到较小的尺寸。 然后,从这个较小的维度尝试使用模型的解码器部分重建输入。 通常用许多名称来称呼这种较小的尺寸,例如潜在空间,隐藏空间,嵌入或编码。
如果自编码器能够再现其输入,则从理论上讲,该潜在空间应该对表示原始数据所需的所有重要信息进行编码,但其优点是尺寸小于输入。 编码器可以被认为是一种压缩输入数据的方式,而解码器是一种将其解压缩的方式。 在下图中,我们可以看到一个简单的自编码器的外观。 我们的潜在空间或编码是中间标记为z
的部分。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-j80GKKKM-1681568428340)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/bace7dc4-4820-4dcd-934d-7215a3d8e4ce.png)]
传统上,在自编码器中,构成网络的层只是全连接层,但是通过使用卷积层,自编码器也可以扩展到图像。 与之前一样,编码器会将输入图像压缩为较小的表示形式,而解码器将尽最大努力恢复信息。 区别在于,编码器现在是将数据压缩为特征向量的 CNN,而不是具有全连接层的 ANN,并且解码器将使用转置的卷积层从编码中重新生成图像。
此处提供了一个自编码器处理图像的示例。 对于解码器部分
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rMm9PB4S-1681568428341)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/460f5473-9a64-4f05-8479-e9f9f36b5eb4.png)]
对于任何自编码器,损失函数都会引导编码器和解码器重建输入。 使用的常见损失是自编码器的输出与网络输入之间的 L2 损失。 我们现在应该问自己一个问题:“使用 L2 损失比较图像是一个好主意吗?”。 如果您拍摄以下图像,即使它们看起来截然不同,它们实际上彼此之间的距离L2
也相同:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wnHmf2Oo-1681568428341)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/1d80926b-a372-459a-8c13-ab3f74dd6fc5.png)]
这表明当您使用 L2 损失比较图像时,并非总是可以依靠它,因此在使用它时应牢记这一点。
卷积自编码器示例
以下 TensorFlow 代码将为 MNIST 数据集构建卷积自编码器模型。 代码的第一部分将构建模型,编码器和解码器的图。 在代码中,我们突出显示模型的一部分,其输出将是我们的潜在向量:
代码语言:javascript复制class CAE_CNN(object):
def __init__(self, img_size = 28, latent_size=20):
self.__x = tf.placeholder(tf.float32, shape=[None, img_size * img_size], name='IMAGE_IN')
self.__x_image = tf.reshape(self.__x, [-1, img_size, img_size, 1])
with tf.name_scope('ENCODER'):
##### ENCODER
# CONV1: Input 28x28x1 after CONV 5x5 P:2 S:2 H_out: 1 (28 4-5)/2 = 14, W_out= 1 (28 4-5)/2 = 14
self.__conv1_act = tf.layers.conv2d(inputs=self.__x_image, strides=(2, 2),
filters=16, kernel_size=[5, 5], padding="same", activation=tf.nn.relu)
# CONV2: Input 14x14x16 after CONV 5x5 P:0 S:2 H_out: 1 (14 4-5)/2 = 7, W_out= 1 (14 4-5)/2 = 7
self.__conv2_act = tf.layers.conv2d(inputs=self.__conv1_act, strides=(2, 2),
filters=32, kernel_size=[5, 5], padding="same", activation=tf.nn.relu)
with tf.name_scope('LATENT'):
# Reshape: Input 7x7x32 after [7x7x32]
self.__enc_out = tf.reshape(self.__conv2_act, [tf.shape(self.__x)[0], 7 * 7 * 32])
self.__guessed_z = tf.layers.dense(inputs=self.__enc_out,
units=latent_size, activation=None, name="latent_var")
tf.summary.histogram("latent", self.__guessed_z)
with tf.name_scope('DECODER'):
##### DECODER (At this point we have 1x18x64
self.__z_develop = tf.layers.dense(inputs=self.__guessed_z,
units=7 * 7 * 32, activation=None, name="z_matrix")
self.__z_develop_act = tf.nn.relu(tf.reshape(self.__z_develop, [tf.shape(self.__x)[0], 7, 7, 32]))
# DECONV1
self.__conv_t2_out_act = tf.layers.conv2d_transpose(inputs=self.__z_develop_act,
strides=(2, 2), kernel_size=[5, 5], filters=16,
padding="same", activation=tf.nn.relu)
# DECONV2
# Model output
self.__y = tf.layers.conv2d_transpose(inputs=self.__conv_t2_out_act,
strides=(2, 2), kernel_size=[5, 5], filters=1,
padding="same", activation=tf.nn.sigmoid)
# We want the output flat for using on the loss
self.__y_flat = tf.reshape(self.__y, [tf.shape(self.__x)[0], 28 * 28])
与卷积自编码器损失有关的代码段如下:
代码语言:javascript复制with tf.name_scope("CAE_LOSS"):
# L2 loss
loss = tf.losses.mean_squared_error(labels=model_in, predictions=model_out_flat)
# Solver configuration
with tf.name_scope("Solver"):
train_step = tf.train.AdamOptimizer(0.0001).minimize(loss)
自编码器的用途和局限性
自编码器的简单性很酷,但是在功能上有所限制。 他们的一个潜在用途是预训练模型(假设您将模型作为编码器部分,并且能够创建反模型作为解码器)。 使用自编码器可以很好地进行预训练,因为您可以获取数据集并训练自编码器以对其进行重构。 训练后,您可以使用编码器的权重,然后将其微调到您要执行的任务。
如果不太复杂,则另一种用途是作为数据压缩形式。 您可以使用自编码器将维数减小到两维或三维,然后尝试在潜在空间中可视化您的输入以查看它是否对您有用。
但是,自编码器的局限性在于它们不能用于为我们生成更多数据。 这是因为我们不知道如何创建新的潜在向量来馈送到解码器。 唯一的方法是在输入数据上使用编码器。 现在,我们将研究对自编码器的修改,以帮助解决此问题。
变分自编码器
我们第一个可以生成更多类似于训练数据的真实生成模型,将是变分自编码器(VAE)。 VAE 看起来像正常的自编码器,但有一个新的约束,它将迫使我们的压缩表示(潜伏空间)遵循零均值和单位方差高斯分布。
在潜在空间上施加此约束的想法是,当我们想使用 VAE 生成新数据时,我们可以创建来自单位高斯分布的样本向量,并将其提供给经过训练的解码器。 VAE 和常规自编码器之间的差异就是对潜在空间向量的约束。 这个约束条件使我们可以创建一种新的潜在向量,然后再将其馈送到解码器以生成数据。
下图显示,VAE 在结构上与自编码器完全相同,除了对隐藏空间的约束之外:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0FuQ4oc9-1681568428341)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/4ea0bf91-eefc-4fd8-bc77-b9205cc8309e.png)]
定义正态分布的参数
我们需要两个参数来跟踪并强制我们的 VAE 模型在潜在空间中产生正态分布:
- 平均值(应为零)
- 标准差(应为 1)
在下图中,我们给出了具有不同均值和标准差值的正态分布示例。 仅使用这两个值,我们就可以产生一个正态分布,可以从中采样:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1j10WS88-1681568428342)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/ce4aa377-1bf4-483a-ab86-23ab2b9dc716.jpg)]
VAE 损失函数
在 VAE 中,损失函数由两部分组成:
- 生成损失:此损失将模型输出与模型输入进行比较。 这可能是我们在自编码器中使用的损失,例如 L2 损失。
- 潜在损失:此损失将潜在向量与零均值,单位方差高斯分布进行比较。 我们在这里使用的损失将是 KL 散度损失。 如果 VAE 开始产生不是来自所需分布的潜在向量,则该损失项将对 VAE 造成不利影响。
以下屏幕截图显示了 VAE 的损失,它是生成损失和潜在空间损失的组合:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FBjwqyd2-1681568428342)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/f07350d3-1230-4c3f-aae8-41f1511e8195.jpg)]
Kullback-Leibler 散度
KL 散度损失将产生一个数字,该数字指示两个分布彼此之间的接近程度。
两个分布之间的距离越近,损失就越低。 在下图中,蓝色分布正在尝试对绿色分布进行建模。 随着蓝色分布越来越接近绿色分布,KL 散度损失将接近于零。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-40md4kmb-1681568428342)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/3f81672f-26ea-446b-97b4-2b6119614be7.png)]
训练 VAE
为了训练 VAE 并使用 KL 散度损失,我们首先需要研究如何生成潜向量。 我们将使编码器产生两个向量,而不是让编码器直接精确地产生一个潜在向量。 第一个是平均值的向量μ
,第二个是标准差值的向量σ
。 根据这些,我们可以创建第三个向量,其中使用μ
和σ
从高斯分布中采样元素向量的第i
个值作为该高斯分布的均值和标准差。 然后,该第三采样向量被发送到解码器。
现在,我们的模型如下所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DAaiSTrQ-1681568428342)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/944095bc-4bde-4e04-86e3-1ccc3e0b1c04.png)]
上图中的均值和标准差块将只是正常的全连接层,它们将通过 KL 损失函数来学习如何返回所需的值。 更改我们如何获得潜向量的原因是因为它使我们能够轻松计算 KL 散度损失。 KL 损失现在如下:latent_mean
为μ
,latent_stddev
为σ
:
0.5 * tf.reduce_sum(tf.square(latent_mean) tf.square(latent_stddev) - tf.log(tf.square(latent_stddev)) - 1, 1)
不幸的是,有一个样本块,您可以将其视为随机生成器节点,无法微分。 这意味着我们不能使用反向传播来训练我们的 VAE。 我们需要一种称为“重新参数化”技巧的东西,该技巧将从反向传播流中取出采样。
重新参数化技巧
重新参数化技巧的想法是从反向传播循环中取出随机样本节点。 它是通过从高斯分布中获取样本ε,然后将其乘以我们的标准差向量σ
的结果,然后加上μ
来实现的。 现在,我们的潜在向量的公式是:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZFCjHms8-1681568428343)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/16915d9b-677d-4f06-b381-1fe5e7353406.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ygxK1ADL-1681568428343)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/ae58ed7b-25c5-447d-bb71-54d32dc9ed15.jpg)]
产生的潜向量将与以前相同,但是现在进行此更改可以使梯度流回到 VAE 的编码器部分。 下图显示了在进行重新参数化之前的 VAE 模型,在左侧进行了重新参数化之后。 蓝色框是损失函数的两个部分。 查看该图,您可以看到我们的梯度现在可以向后流动,因为我们不再具有红色框(示例节点)来挡路了:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8dCi90AV-1681568428343)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/412bfece-2f71-4956-a5fe-3ae19cce2b6b.png)]
这是 TensorFlow 中的重新参数化形式:
代码语言:javascript复制# Add linear ops to produce mean and standard devation vectors
fc_mean = tf.layers.dense(self.__enc_out, units=latent_size, activation=None, name="w_mean")
fc_stddev = tf.layers.dense(self.__enc_out, units=latent_size, activation=None, name="w_stddev")
# Generate normal distribution with dimensions [Batch, latent_size]
sample_block = tf.random_normal([tf.shape(X)[0], latent_size], 0, 1, dtype=tf.float32)
latent_z = fc_mean (fc_stddev * sample_block)
卷积变分自编码器代码
现在我们可以将所有内容组合在一起,并提供 TensorFlow 代码,这些代码将为 MNIST 数据集构建卷积 VAE。 我们为 VAE 模型创建一个类,然后将该模型放入__init__
方法中。 第一部分是我们的模型的编码器,由两个转换层组成:
class VAE_CNN(object):
def __init__(self, img_size=28, latent_size=20):
self.__x = tf.placeholder(tf.float32, shape=[None, img_size * img_size], name='IMAGE_IN')
self.__x_image = tf.reshape(self.__x, [-1, img_size, img_size, 1])
with tf.name_scope('ENCODER'):
##### ENCODER
# CONV1: Input 28x28x1 after CONV 5x5 P:2 S:2 H_out: 1 (28 4-5)/2 = 14, W_out= 1 (28 4-5)/2 = 14
self.__conv1_act = tf.layers.conv2d(inputs=self.__x_image, strides=(2, 2),
filters=16, kernel_size=[5, 5], padding="same", activation=tf.nn.relu)
# CONV2: Input 14x14x16 after CONV 5x5 P:0 S:2 H_out: 1 (14 4-5)/2 = 7, W_out= 1 (14 4-5)/2 = 7
self.__conv2_act = tf.layers.conv2d(inputs=self.__conv1_act, strides=(2, 2),
filters=32, kernel_size=[5, 5], padding="same", activation=tf.nn.relu)
接下来是 VAE 的一部分,该部分负责使用我们之前的新重新参数化技巧来创建潜在向量。 我们添加了对最终潜在向量的记录,以检查它是否按照我们期望的那样遵循单位高斯分布产生向量:
代码语言:javascript复制 with tf.name_scope('LATENT'):
# Reshape: Input 7x7x32 after [7x7x32]
self.__enc_out = tf.reshape(self.__conv2_act, [tf.shape(self.__x)[0], 7 * 7 * 32])
# Add linear ops for mean and variance
self.__w_mean = tf.layers.dense(inputs=self.__enc_out,
units=latent_size, activation=None, name="w_mean")
self.__w_stddev = tf.layers.dense(inputs=self.__enc_out,
units=latent_size, activation=None, name="w_stddev")
# Generate normal distribution with dimensions [B, latent_size]
self.__samples = tf.random_normal([tf.shape(self.__x)[0], latent_size], 0, 1, dtype=tf.float32)
self.__guessed_z = self.__w_mean (self.__w_stddev * self.__samples)
tf.summary.histogram("latent_sample", self.__guessed_z)
之后,我们添加网络的解码器部分,该部分包括一个全连接层,然后是两个转置的卷积层:
代码语言:javascript复制 with tf.name_scope('DECODER'):
##### DECODER
# Linear layer
self.__z_develop = tf.layers.dense(inputs=self.__guessed_z,
units=7 * 7 * 32, activation=None, name="z_matrix")
self.__z_develop_act = tf.nn.relu(tf.reshape(self.__z_develop, [tf.shape(self.__x)[0], 7, 7, 32]))
# DECONV1
self.__conv_t2_out_act = tf.layers.conv2d_transpose(inputs=self.__z_develop_act,
strides=(2, 2), kernel_size=[5, 5], filters=16,
padding="same", activation=tf.nn.relu)
# DECONV2
# Model output
self.__y = tf.layers.conv2d_transpose(inputs=self.__conv_t2_out_act,
strides=(2, 2), kernel_size=[5, 5], filters=1,
padding="same", activation=tf.nn.sigmoid)
# Model output
self.__y_flat = tf.reshape(self.__y, [tf.shape(self.__x)[0], 28 * 28])
与我们的模型分开,我们需要写出最终损失函数,该函数将用于训练 VAE。 然后,我们可以将这种损失传递给我们选择的优化器,以创建我们的训练步骤:
代码语言:javascript复制# Loss function
with tf.name_scope("VAE_LOSS"):
# L2 loss (generative loss)
generation_loss = tf.losses.mean_squared_error(labels=model_in, predictions= model_out_flat)
# KL Loss (latent loss)
latent_loss = 0.5 * tf.reduce_sum(tf.square(z_mean) tf.square(z_stddev) - tf.log(tf.square(z_stddev)) - 1, 1)
# Merge the losses
代码语言:javascript复制 loss = tf.reduce_mean(generation_loss latent_loss)
# Solver
with tf.name_scope("Solver"):
train_step = tf.train.AdamOptimizer(0.0001).minimize(loss)
产生新数据
训练完 VAE 模型后,我们可以将其解码器部分截断,并用作生成器为我们生成新数据。 它将通过向它提供来自单位高斯分布的新潜在向量来工作。
我们在 TensorFlow 中提供负责构建此生成的 VAE 图的代码,如下所示:
代码语言:javascript复制class VAE_CNN_GEN(object):
def __init__(self, img_size=28, latent_size=20):
self.__x = tf.placeholder(tf.float32, shape=[None, latent_size], name='LATENT_IN')
with tf.name_scope('DECODER'):
# Linear layer
self.__z_develop = tf.layers.dense(inputs=self.__x,
units=7 * 7 * 32, activation=None, name="z_matrix")
self.__z_develop_act = tf.nn.relu(tf.reshape(self.__z_develop, [tf.shape(self.__x)[0], 7, 7, 32]))
# DECONV1
self.__conv_t2_out_act = tf.layers.conv2d_transpose(inputs=self.__z_develop_act,
strides=(2, 2), kernel_size=[5, 5], filters=16,
padding="same", activation=tf.nn.relu)
# DECONV2
# Model output
self.__y = tf.layers.conv2d_transpose(inputs=self.__conv_t2_out_act,
strides=(2, 2), kernel_size=[5, 5], filters=1,
padding="same", activation=tf.nn.sigmoid)
@property
def output(self):
return self.__y
@property
def input(self):
return self.__x
生成对抗网络
生成对抗网络(GAN)是另一种非常新的生成模型,由于其令人印象深刻的结果而受到关注。 GAN 由两个网络组成:生成器网络和判别器网络。 在训练过程中,他们俩都玩零和游戏,其中判别器网络试图发现输入到其中的图像是真实的还是伪造的。 同时,生成器网络尝试创建足以欺骗判别器的伪造图像。
想法是经过一段时间的训练,判别器和生成器都非常擅长于他们的任务。 结果,生成器被迫尝试创建看起来越来越接近原始数据集的图像。 为此,它必须捕获数据集的概率分布。
下图概述了此 GAN 模型的外观:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1gdUWnHM-1681568428343)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/98fcbf21-46e3-45f0-8a2d-a7004af773c3.png)]
判别器和生成器都将具有自己的损失函数,但是它们的损失都相互依赖。
让我们总结一下 GAN 模型的两个主要模块或网络:
- 生成器:使用大小为 N 的一维向量作为输入,创建类似于真实图像数据集的图像(选择 N 取决于我们)
- 判别器:验证提供给它的图像是真实的还是伪造的
GAN 的一些实际用法如下:
- 使用判别器网络权重作为不同任务的初始化,类似于我们对自编码器可以执行的操作
- 使用生成器网络创建新图像,可能会扩大您的数据集,就像我们可以使用经过训练的 VAE 解码器一样
- 将判别器用作损失函数(对于图像,可能优于 L1/L2),并且也可以在 VAE 中使用
- 通过将生成的数据与标记的数据混合来进行半监督学习
现在我们将向您展示如何在 TensorFlow 中实现非常简单的 GAN。 一旦经过训练,我们的 GAN 的生成器部分就可以用于根据 100 个长随机噪声向量创建 MNIST 手写数字。 让我们开始吧!
判别器
我们需要做的第一件事就是创建我们的判别网络。 为此,我们将几个全连接层堆叠在一起。 判别器将 784 个长度向量作为输入,这是我们的28x28
MNIST 图像变平。 每个图像的输出将只是一个数字,这是鉴别者对该图像为真实图像的信心程度的分数。 我们使用 Leaky ReLu 作为激活函数,以防止 ReLu 单元死亡。
我们返回原始对率,因为损失函数将为我们应用 Sigmoid 激活函数,以确保判别器输出在 0 到 1 之间:
代码语言:javascript复制def discriminator(x):
with tf.variable_scope("discriminator"):
fc1 = tf.layers.dense(inputs=x, units=256, activation=tf.nn.leaky_relu)
fc2 = tf.layers.dense(inputs=fc1, units=256, activation=tf.nn.leaky_relu)
logits = tf.layers.dense(inputs=fc2, units=1)
return logits
生成器
现在我们创建生成器网络。 生成器的工作是将随机噪声的向量作为输入,并从中生成输出图像。 在此示例中,我们再次使用全连接层,这些层最后将产生 784 个长向量的输出,我们可以对其进行整形以获得28x28
的图像:
def generator(z):
with tf.variable_scope("generator"):
fc1 = tf.layers.dense(inputs=z, units=1024, activation=tf.nn.relu)
fc2 = tf.layers.dense(inputs=fc1, units=1024, activation=tf.nn.relu)
img = tf.layers.dense(inputs=fc2, units=784, activation=tf.nn.tanh)
return img
我们在输出上使用 tanh 激活来将生成的图像限制在 -1 到 1 的范围内。
现在已经定义了模型,我们可以看看 GAN 训练所需的损失函数。
GAN 损失函数
如前所述,判别器和生成器都有自己的损失函数,这些函数取决于彼此网络的输出。 我们可以将 GAN 视为在判别器和生成器之间玩 minimax 游戏,如下所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ovi8GjCH-1681568428344)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/1d24b043-c4b8-42f0-8f9c-8f2c7db22ec3.png)]
在这里,D
是我们的判别器,G
是我们的生成器,z
是输入到生成器的随机向量,x
是真实图像。 尽管我们在此处给出了 GAN 损失的总和,但实际上更容易分别考虑这两种优化。
为了训练 GAN,我们将在判别器和生成器之间交替进行梯度步骤更新。 在更新判别器时,我们要尝试使最大化判别器做出正确选择的概率。 在更新生成器时,我们想尝试使最小化判别器做出正确选择的可能性。
但是,为了实际实现,我们将与之前给出的内容相比,稍微改变 GAN 损失函数; 这样做是为了帮助训练收敛。 变化是,当更新生成器时,而不是最小化判别器做出正确选择的可能性; 我们改为最大化判别器做出错误选择的概率:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HdeKTBmK-1681568428344)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/5222b764-d50d-4fbc-9747-47a475579e50.png)]
更新判别器时,我们尝试使最大化,它对真实数据和伪数据均做出正确选择的可能性:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9RNqAkB7-1681568428344)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/82987b0d-594f-4f08-9c76-436c0ec89fbc.png)]
生成器损失
生成器想要欺骗判别器,换句话说,使判别器输出q
用于生成的图像G(z)
。 生成器损失只是施加到生成器结果的判别器输出的二项式交叉熵损失的负值。 请注意,由于生成器始终尝试生成“真实”图像,因此交叉熵损失可简化为:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P37yG78P-1681568428345)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/1761cebc-4eca-4a0f-81cf-9a128632ee81.png)]
在这里,每项的含义如下:
m
:批量D
:判别器G
:生成器z
:随机噪声向量
我们想在训练 GAN 时最大化损失函数。 当损失最大化时,这意味着生成器能够生成可能使判别器蒙蔽的图像,并且判别器针对生成的图像输出 1。
判别器损失
鉴别者希望能够区分真实图像和生成图像。 它想为真实图像输出 1,为生成图像输出 0。 判别器损失函数具有以下公式,由于 GAN 训练和标记的工作方式而再次简化:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IOkxZdfo-1681568428345)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/6afbb7c8-d706-49dc-b74b-f8d9f894e8d3.png)]
此损失函数有两项:
- 应用于判别器模型的二项式交叉熵产生了一些真实数据
x
- 将二项式交叉熵应用于所生成数据
G(z)
的判别器模型结果
如前所述,我们采取这些不利条件,并希望在训练 GAN 时最大化此损失函数。 当这种损失最大化时,这意味着判别器能够区分实际输出和生成的输出。 注意,当判别器对于真实图像输出 1 而对于所生成图像输出 0 时,该损失最大。
综合损失
在 TensorFlow 中,我们可以实现整个 GAN 损失,如以下代码所示。 作为输入,我们从判别器的输出中获取来自生成器的一批伪图像和来自我们的数据集的一批真实图像:
代码语言:javascript复制def gan_loss(logits_real, logits_fake):
# Target label vectors for generator and discriminator losses.
true_labels = tf.ones_like(logits_real)
fake_labels = tf.zeros_like(logits_fake)
# DISCRIMINATOR loss has 2 parts: how well it classifies real images and how well it
# classifies fake images.
real_image_loss = tf.nn.sigmoid_cross_entropy_with_logits(logits=logits_real, labels=true_labels)
fake_image_loss = tf.nn.sigmoid_cross_entropy_with_logits(logits=logits_fake, labels=fake_labels)
# Combine and average losses over the batch
discriminator_loss = tf.reduce_mean(real_image_loss fake_image_loss)
# GENERATOR is trying to make the discriminator output 1 for all its images.
# So we use our target label vector of ones for computing generator loss.
generator_loss = tf.nn.sigmoid_cross_entropy_with_logits(logits=logits_fake, labels=true_labels)
# Average generator loss over the batch.
generator_loss = tf.reduce_mean(G_loss)
return discriminator_loss , generator_loss
您可能已经注意到,不可能同时最大化判别器损失和生成器损失。 这就是 GAN 的优点,因为在训练时,该模型有望达到某种平衡,在这种情况下,生成器必须生成真正高质量的图像,以欺骗判别器。
TensorFlow 仅允许其优化器最小化而不是最大化。 结果,我们实际上采用了前面所述的损失函数的负值,这意味着我们从最大化损失变为最小化损失。 不过,我们无需执行任何其他操作,因为tf.nn.sigmoid_cross_entropy_with_logits()
会为我们解决此问题。
训练 GAN
因此,现在有了生成器,判别器和损失函数,剩下的就是训练! 我们将在 TensorFlow 中给出如何执行此操作的草图,因为这部分没有花哨的内容。 就像我们之前所做的那样,它只是将上一节中的内容以及加载和馈送 MNIST 图像拼凑在一起。
首先,设置两个求解器:一个用于判别器,一个用于生成器。 已显示AdamOptimizer
的较小值beta1
,因为它已显示出可帮助 GAN 训练收敛:
discriminator_solver = tf.train.AdamOptimizer(learning_rate=0.001, beta1=0.5)
generator_solver = tf.train.AdamOptimizer(learning_rate=0.001, beta1=0.5)
接下来,创建一个随机噪声向量; 这可以通过tf.random_uniform
完成。 这被馈送到生成器网络以创建一批生成的图像:
z = tf.random_uniform(maxval=1,minval=-1,shape=[batch_size, dim])
generator_sample = generator(z)
然后,我们将一批真实图像和一批生成的样本提供给判别器。 我们在这里使用变量范围来重用我们的模型变量,并确保不创建第二个图:
代码语言:javascript复制 with tf.variable_scope("") as scope:
logits_real = discriminator(x)
# We want to re-use the discriminator weights.
scope.reuse_variables()
logits_fake = discriminator(generator_sample )
由于需要分别更新它们,因此我们将判别器和生成器的权重分开:
代码语言:javascript复制discriminator_vars = tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES, 'discriminator')
generator_vars = tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES, 'generator')
最后,我们计算损失并将其与相关权重一起发送给优化器以进行更新:
代码语言:javascript复制 discriminator_loss, generator_loss = gan_loss(logits_real, logits_fake)
# Training steps.
discriminator_train_step = discriminator_solver.minimize(discriminator_loss, var_list=discriminator_vars )
generator_train_step = generator_solver.minimize(generator_loss , var_list=generator_vars )
这些是训练 GAN 的主要步骤。 剩下的就是创建一个训练循环,遍历大量数据。 如果这样做,您应该能够像训练中那样输入任何随机噪声向量,并生成图像。
如下图所示,创建的图像开始类似于 MNIST 数字:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F2FVKwnh-1681568428345)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/b6e289a6-0c13-4ee0-bb09-78bd4387231b.png)]
深度卷积 GAN
深度卷积 GAN(DCGAN)是我们之前看到的普通 GAN 的扩展。 我们不是使用全连接层,而是使用卷积层。 想法是使用卷积层可以帮助生成器形成更好的图像。 以下是这种模型的示例:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fOV1443a-1681568428345)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/b9cf2f8e-db74-44ab-8090-d032d5a82d24.jpg)]
DCGAN 的示例实现与之前训练普通 GAN 相同,只是简单地将判别器和生成器网络换成一些卷积架构,如以下代码所示。 请注意,生成器将使用转置卷积来上采样:
代码语言:javascript复制def discriminator(x):
with tf.variable_scope("discriminator"):
unflatten = tf.reshape(x, shape=[-1, 28, 28, 1])
conv1 = tf.layers.conv2d(inputs=unflatten, kernel_size=5, strides=1, filters=32 ,activation=leaky_relu)
maxpool1 = tf.layers.max_pooling2d(inputs=conv1, pool_size=2, strides=2)
conv2 = tf.layers.conv2d(inputs=maxpool1, kernel_size=5, strides=1, filters=64,activation=leaky_relu)
maxpool2 = tf.layers.max_pooling2d(inputs=conv2, pool_size=2, strides=2)
flatten = tf.reshape(maxpool2, shape=[-1, 1024])
fc1 = tf.layers.dense(inputs=flatten, units=1024, activation=leaky_relu)
logits = tf.layers.dense(inputs=fc1, units=1)
return logits
代码语言:javascript复制def generator(z):
with tf.variable_scope("generator"):
fc1 = tf.layers.dense(inputs=z, units=1024, activation=tf.nn.relu)
bn1 = tf.layers.batch_normalization(inputs=fc1, training=True)
fc2 = tf.layers.dense(inputs=bn1, units=7*7*128, activation=tf.nn.relu)
bn2 = tf.layers.batch_normalization(inputs=fc2, training=True)
reshaped = tf.reshape(bn2, shape=[-1, 7, 7, 128])
conv_transpose1 = tf.layers.conv2d_transpose(inputs=reshaped, filters=64, kernel_size=4, strides=2, activation=tf.nn.relu,
padding='same')
bn3 = tf.layers.batch_normalization(inputs=conv_transpose1, training=True)
conv_transpose2 = tf.layers.conv2d_transpose(inputs=bn3, filters=1, kernel_size=4, strides=2, activation=tf.nn.tanh,
padding='same')
img = tf.reshape(conv_transpose2, shape=[-1, 784])
return img
只需换出生成器和判别器网络以使用卷积运算,我们就能生成如下图像:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uFl1RDtR-1681568428346)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/ada0a5b7-1268-4f61-820a-3aca71dba80f.png)]
现在产生的质量非常好,几乎与真实数据没有区别。 另外,请注意,图像确实非常清晰,并且没有像我们之前那样模糊且几乎没有伪影。
需要注意的几点是:
- 对于判别器:再次使用泄漏的 relu,不要使用最大池。 仅使用跨步卷积或平均池。
- 对于生成器:在最后一层使用 relu 和 tanh。
- 通常,最佳实践是在生成器和判别器上都使用批量规范化层。 它们将始终设置为训练模式。
- 有时,人们运行生成器优化器的次数是运行判别器优化器的两倍。
这是一个简单的 DCGAN 在生成人脸图像时可以达到的质量的示例:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LMBWFXSd-1681568428346)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/a71cc7eb-9f5c-4bbc-b4f2-9bb2631bd7c8.jpg)]
WGAN
Wasserstein GAN 是 GAN 的另一种变体,它解决了训练 GAN 时可能发生的问题,即模式崩溃。 此外,其目的在于给出一种度量,该度量指示 GAN 何时收敛,换句话说,损失函数具有该值的含义。
重要的更改是从损失中删除对数并修剪判别器权重。
此外,请按照下列步骤操作:
- 训练判别器比生成器更多
- 减少判别器的权重
- 使用 RMSProp 代替 Adam
- 使用低学习率(0.0005)
WGAN 的缺点是训练起来较慢:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nndLLplk-1681568428346)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/6cfab511-a579-49c1-96bb-64870fb2cfd0.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Az3UoKBB-1681568428346)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/8a6bd68f-88df-4180-a3fd-18487b4701a5.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2EPEKR10-1681568428347)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/df33e809-e094-4a71-acf8-1ebe3beb0afc.png)]
WGAN 产生的图像结果仍然不是很好,但是该模型确实有助于解决模式崩溃问题。
BEGAN
BEGAN 的主要思想是在判别器上使用自编码器,这将有其自身的损失,该损失会衡量自编码器对某些图像(生成器或真实数据)的重构程度:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TYW29cnP-1681568428347)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/1a6f3311-6e16-492c-860d-45414eedbc67.jpg)]
BEGAN 的一些优点如下:
- 高分辨率(
128x128
)人脸生成(2017 最新技术)。 - 提供一种衡量收敛的方法。
- 即使没有批量规范和丢弃法也有良好的结果。
- 超参数可控制世代多样性与质量。 更高的质量也意味着更多的模式崩溃。
- 不需要两个单独的优化器。
这是 BEGAN 在生成人脸任务时可以生成的图像质量的示例:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-STysAwmE-1681568428347)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/bf3a0b45-8535-40ec-93ed-fb4ca994202c.jpg)]
条件 GAN
条件 GAN 是普通 GAN 的扩展,其中判别器和生成器都被设置为某些特定类别y
。这具有有趣的应用,因为您可以将生成器固定到特定的类,然后使其生成我们选择的特定同一类的多个不同版本。 例如,如果将y
设置为与 MNIST 中的数字 7 对应的标签,则生成器将仅生成 7 的图像。
使用条件 GAN,minimax 游戏变为:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9DmNG3Zu-1681568428347)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/ade35614-04cc-4447-b1a0-b8e94dc0ce7a.png)]
在这里,我们依赖于额外输入y
,它是输入图像的类标签。
合并x
和y
,或z
和y
的最简单方法是将它们连接在一起,这样我们的输入向量就更长。 这将创建一个更加受控制的数据集扩充系统。 这是 TensorFlow 代码中的一个示例:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2ZPsiHOf-1681568428347)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/63ca28dd-5605-482d-95b3-4e1c027f3c89.jpg)]
GAN 的问题
GAN 当前最大的问题是,它们很难训练。 幸运的是,有一些技术可以使事情变得容易,这是目前非常活跃的研究领域。
在这里,我们将介绍训练 GAN 的一些问题以及如何解决它们。
损失可解释性
训练 GAN 时的问题之一是,生成器损失和判别器损失的值都没有明显的影响。 这不像训练分类器,只是等待损失下降以查看模型是否在训练。
对于 GAN,损失值的下降并不一定意味着该模型正在训练中:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LKC6hpQM-1681568428348)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/c8fb861d-7d21-463d-bf33-1b1398362b00.jpg)]
通过许多人的实验和研究,以下是有关如何使用 GAN 损失值的一些提示:
- 您不希望判别器的损失下降得很快,因为它将无法向生成器提供反馈以改善它。
- 如果生成器损失迅速下降,则意味着它发现了一个判别器弱点,并一次又一次地利用了这一弱点。 如果发生这种情况,则称为模式折叠。
损失实际上仅对查看训练中是否出现问题有好处。 因此,没有很好的方法知道训练已经收敛。 通常,最好的办法是继续查看生成器的输出。 确保输出看起来与您的期望接近,并且输出种类丰富。
模式崩溃
这可能是您在训练 GAN 时遇到的第一个问题。 当生成器找到一组特定的输入来欺骗判别器时,就会发生模式崩溃,并且它会继续利用这种故障情况并将潜伏Z
空间中的许多值折叠为相同的值。
解决此问题的一种方法是使用“小批量功能”或“展开 GANs”,或者完全停止训练,然后在生成器开始创建非常狭窄的输出分布时重新开始:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XEW6ZvWt-1681568428348)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/febfacea-0ff5-4ee3-9fec-729292f70e27.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ckB4fURc-1681568428348)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/418601cc-31d2-4de7-b124-9d1ead1cf29a.jpg)]
改善 GAN 的可训练性的技术
在这里,我们将介绍一些在训练 GAN 时使生活更轻松的技术:
- 归一化输入到 -1/1 之间
- 使用 BatchNorm
- 使用 Leaky Relu(判别器)
- 在生成器输出上使用 Relu(生成器),tanh
- 对于下采样,请使用平均池化或跨步卷积
- 使用 Adam 优化器
- 如果判别器损失迅速下降,则说明存在问题
- 在生成器上使用压降(在训练阶段和测试阶段)
小批量判别器
用于改善模式崩溃的一些技术如下:
- 取得判别器某层的输出
- 将判别器的输入重塑为矩阵
- 计算 L1 距离
- 计算 L1 距离的指数和
- 将结果连接到输入(判别器的某些层)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MTwnh6eo-1681568428348)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/9f716a8f-5a7f-40fc-94fc-ae5f004e4724.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uHMKNFwo-1681568428349)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/375e4e8f-7e97-4d2a-b8f1-b197a83021a2.jpg)]
总结
在本章中,我们了解了生成模型及其与判别模型的不同之处。 我们还讨论了各种自编码器,包括深度,变体和卷积。 此外,我们了解了一种新型的生成模型,称为生成对抗网络(GAN)。 在了解了所有这些生成模型之后,我们看到了如何在 TensorFlow 中自己训练它们以生成手写数字,并看到了它们可以产生的不同质量的图像。
在第 7 章,“迁移学习”中,我们将学习迁移学习及其如何帮助我们加快训练速度。
七、迁移学习
迁移学习的作用恰如其名。 这个想法是将从一项任务中学到的东西迁移到另一项任务上。 为什么? 实际上,每次都从头开始训练整个模型的效率很低,其成功取决于许多因素。 另一个重要原因是,对于某些应用,公开可用的数据集不够大,无法训练出像 AlexNet 或 ResNet 这样的深层架构而又不会过拟合,这意味着无法推广。 示例应用可以从用户给出的一些示例中进行在线学习,也可以是细粒度的分类,其中类别之间的差异很小。
一个非常有趣的观察结果是,由于您冻结了所有其余部分(无论是检测还是分类),最终的层可以用于完成不同的任务,最终权重看起来非常相似。
这导致了迁移学习的想法。 这意味着在大量数据上训练的深度架构(例如 ImageNet)可以很好地概括化,以至于其卷积权重可以充当特征提取器,类似于常规的视觉表示,并且可以用于为各种任务训练线性分类器。
本章旨在教读者如何在 TensorFlow 中采用现成的训练有素的模型,更改其结构以及为特定任务重新训练某些层。 我们将看到迁移学习将如何帮助改善结果并加快训练时间。
本章涵盖的主要主题如下:
- 使用来自另一个训练过的模型的权重预先初始化一个模型
- 在需要时使用 TensorFlow 加载模型并冻结/解冻层
什么时候?
研究表明,在 ImageNet 上训练的卷积网络权重中的特征提取优于常规特征提取方法,例如 SURF,可变形部分描述符(DPD),直方图定向梯度(HOG)和词袋(BoW)。 这意味着无论常规视觉表示如何工作,卷积特征都可以同样好地使用,唯一的缺点是更深的架构可能需要更长的时间来提取特征。
当在 ImageNet 上训练深层卷积神经网络时,第一层中的卷积过滤器的可视化(请参见下图)显示,他们学习了低层特征,类似于边检测过滤器,而卷积过滤器在最后一层学习高级功能,这些功能捕获特定于类的信息。 因此,如果我们在第一个池化层之后提取 ImageNet 的特征并将其嵌入 2D 空间(例如,使用 t-SNE),则可视化将显示数据中存在一些无中心状态,而如果在全连接层上执行相同操作,我们将注意到具有相同语义信息的数据被组织成簇。 这意味着网络可以在更高层次上很好地概括,并且有可能将这种知识迁移到看不见的类别中。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kk0992Ab-1681568428349)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/b7b186d6-7803-4517-add9-f559586b2576.png)]
根据对与 ImageNet 相似度较小的数据集进行的实验,在以下任务上,基于 ImageNet 训练的基于卷积神经网络权重的特征比常规特征提取方法的表现更好:
- 对象识别:此 CNN 特征提取器可以成功地对其他类别不可见的数据集执行分类任务。
- 域适应:这是当训练和测试数据来自不同的分布,而标签和类别数相同时。 不同的域可以考虑使用不同的设备或在不同的设置和环境条件下捕获的图像。 具有 CNN 功能的线性分类器可以在不同域中成功地将具有相同语义信息的图像聚类,而 SURF 功能则可以针对特定领域的特征进行过拟合。
- 精细分类:这是我们要在同一高级类中的子类之间进行分类的时候。 例如,我们可以对鸟类进行分类。 尽管没有对细粒度数据进行训练,但 CNN 功能以及逻辑回归功能的表现优于基线方法。
- 场景识别:在这里,我们需要对整个图像的场景进行分类。 在对象分类数据库上经过训练的 CNN 特征提取器,顶部带有一个简单的线性分类器,其表现优于应用于识别数据的传统特征提取器的复杂学习算法。
这里提到的某些任务与图像分类没有直接关系,图像分类是 ImageNet 训练的主要目标,因此有人希望 CNN 功能无法推广到看不见的场景。 但是,这些功能与简单的线性分类器相结合,表现优于手工制作的功能。 这意味着 CNN 的学习权重是可重用的。
那么什么时候应该使用迁移学习呢? 当我们有一个任务时,由于问题的性质,可用数据集很小(例如对蚂蚁/蜜蜂进行分类)。 在这种情况下,我们可以在包含相似语义信息的较大数据集上训练我们的模型,然后用较小的数据集仅训练最后一层(线性分类器)。 如果我们只有足够的可用数据,并且有一个更大的相似数据集,则对该相似数据集进行预训练可能会导致模型更健壮。 通常情况下,我们使用随机初始化的权重来训练模型,在这种情况下,将使用在其他数据集上训练过的权重来初始化模型。 这将有助于网络更快地融合并更好地推广。 在这种情况下,仅微调模型顶端的几层是有意义的。
经验法则是,从网络顶部开始,可用数据越多,可以训练的层就越多。 通过预训练(例如在 ImageNet 上)模型初始化模型权重。
怎么样? 概述
我们应该如何使用转学? 有两种典型的解决方法。 第一种不太及时的方法是使用所谓的预训练模型,即预先在大型数据集(例如 ImageNet 数据集)上训练过的模型。 这些经过预先训练的模型可以在不同的深度学习框架中轻松获得,并且通常被称为“模型动物园”。 预训练模型的选择在很大程度上取决于当前要解决的任务是什么,以及数据集的大小。 选择模型后,我们可以使用全部或部分模型作为要解决的实际任务的初始化模型。
深度学习的另一种不太常见的方式是自己预先训练模型。 当可用的预训练网络不适合解决特定问题时,通常会发生这种情况,我们必须自己设计网络架构。 显然,这需要更多的时间和精力来设计模型和准备数据集。 在某些情况下,用于进行网络预训练的数据集甚至可以是合成的,可以从计算机图形引擎(例如 3D Studio Max 或 Unity)或其他卷积神经网络(例如 GAN)生成。 可以对虚拟数据进行预训练的模型在真实数据上进行微调,并且可以与仅对真实数据进行训练的模型一起很好地工作。
例如,如果我们想区分猫和狗,而我们没有足够的数据,则可以从“模型动物园”下载在 ImageNet 上训练的网络,并使用除最后一层以外的所有层的权重。 最后一层必须调整为具有与类数量相同的大小,在本例中为两个,并且权重需要重新初始化和训练。 这样,通过将这些层的学习率设置为零或非常小的值(请参见下图),我们将冻结那些不需训练的层。 如果有更大的数据集,我们可以训练最后三个全连接层。 有时,预训练网络只能用于初始化权重,然后再进行正常训练。
迁移学习之所以有效,是因为在初始层计算出的特征更通用并且看起来很相似。 在顶层提取的特征对于我们要解决的问题变得更加具体。
为了进一步研究如何使用迁移学习,以及对该主题的更深刻理解,让我们看一下代码。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hSGjbLWz-1681568428349)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/a6a25c2f-05f4-4396-8db5-9a2fe9c555dd.png)]
怎么样? 代码示例
在本节中,我们将学习在 TensorFlow 中进行迁移学习所需的实用技能。 更具体地说,我们将学习如何从检查点选择要加载的层,以及如何指示我们的求解器仅优化特定的层而冻结其他层。
TensorFlow 有用的元素
由于迁移学习是关于训练一个网络的权重,而该网络已从另一个训练后的模型中获取了权重,因此我们将需要找到一个。 在我们的示例中,我们将使用预训练卷积自编码器的编码部分,该部分在第 6 章中进行了说明。 使用自编码器的优点是我们不需要标记的数据,也就是说,可以完全不受监督地对其进行训练。
没有解码器的自编码器
包含两个卷积层和一个完全连接层的编码器(不带解码器部分的自编码器)如下所示。 父自编码器在 MNIST 数据集上进行了训练。 因此,网络将大小为28x28x1
的图像作为输入,并在潜在空间将其编码为 10 维向量,每个类别一维:
# Only half of the autoencoder changed for classification
class CAE_CNN_Encoder(object):
......
def build_graph(self, img_size=28):
self.__x = tf.placeholder(tf.float32, shape=[None, img_size * img_size], name='IMAGE_IN')
self.__x_image = tf.reshape(self.__x, [-1, img_size, img_size, 1])
self.__y_ = tf.placeholder("float", shape=[None, 10], name='Y')
with tf.name_scope('ENCODER'):
##### ENCODER
# CONV1: Input 28x28x1 after CONV 5x5 P:2 S:2 H_out: 1 (28 4-5)/2 = 14,
# W_out= 1 (28 4-5)/2 = 14
self.__conv1_act = tf.layers.conv2d(inputs=self.__x_image, strides=(2, 2), name='conv1',
filters=16, kernel_size=[5, 5], padding="same", activation=tf.nn.relu)
# CONV2: Input 14x14x16 after CONV 5x5 P:0 S:2 H_out: 1 (14 4-5)/2 = 7,
# W_out= 1 (14 4-5)/2 = 7
self.__conv2_act = tf.layers.conv2d(inputs=self.__conv1_act, strides=(2, 2),
name='conv2', filters=32, kernel_size=[5, 5], padding="same", activation=tf.nn.relu)
with tf.name_scope('LATENT'):
# Reshape: Input 7x7x32 after [7x7x32]
self.__enc_out = tf.layers.flatten(self.__conv2_act, name='flatten_conv2')
self.__dense = tf.layers.dense(inputs=self.__enc_out, units=200, activation=tf.nn.relu, name='fc1')
self.__logits = tf.layers.dense(inputs=self.__dense, units=10, name='logits')
def __init__(self, img_size=28):
if CAE_CNN_Encoder.__instance is None:
self.build_graph(img_size)
@property
def output(self):
return self.__logits
@property
def labels(self):
return self.__y_
@property
def input(self):
return self.__x
@property
def image_in(self):
return self.__x_image
选择层
一旦定义了模型model = CAE_CNN_Encoder
()
,选择将要使用预训练权重初始化的层就很重要。 请注意,两个网络的结构(要初始化的网络和提供训练后的权重的网络)必须相同。 因此,例如,以下代码片段将选择名称为convs
为fc
的所有层:
from models import CAE_CNN_Encoder
model = CAE_CNN_Encoder()
list_convs = [v for v in tf.global_variables() if "conv" in v.name]
list_fc_linear = [v for v in tf.global_variables() if "fc" in v.name or "output" in v.name]
请注意,这些列表是从tf.global_variables()
填充的; 如果选择打印其内容,则可能会发现它包含所有模型变量,如下所示:
[<tf.Variable 'conv1/kernel:0' shape=(5, 5, 1, 16) dtype=float32_ref>,
<tf.Variable 'conv1/bias:0' shape=(16,) dtype=float32_ref>,
<tf.Variable 'conv2/kernel:0' shape=(5, 5, 16, 32) dtype=float32_ref>,
<tf.Variable 'conv2/bias:0' shape=(32,) dtype=float32_ref>,
<tf.Variable 'fc1/kernel:0' shape=(1568, 200) dtype=float32_ref>,
<tf.Variable 'fc1/bias:0' shape=(200,) dtype=float32_ref>,
<tf.Variable 'logits/kernel:0' shape=(200, 10) dtype=float32_ref>,
<tf.Variable 'logits/bias:0' shape=(10,) dtype=float32_ref>]
将定义图的层分为卷积和完全连接两个列表后,您将使用tf.Train.Saver
加载所需的权重。 首先,我们需要创建一个保存器对象,将要从检查点加载的变量列表作为输入,如下所示:
# Define the saver object to load only the conv variables
saver_load_autoencoder = tf.train.Saver(var_list=list_convs)
除了saver_load_autoencoder
,我们还需要创建另一个saver
对象,该对象将允许我们将要训练的网络的所有变量存储到检查点中。
# Define saver object to save all the variables during training
saver = tf.train.Saver()
然后,在使用init = tf.global_variables_initializer()
初始化图并创建会话之后,我们可以使用saver_load_autoencoder
从检查点恢复卷积层,如下所示:
# Restore only the weights (From AutoEncoder)
saver_load_autoencoder.restore(sess, "../tmp/cae_cnn/model.ckpt-34")
请注意,调用restore
会覆盖global_variables_initializer
,所有选定的权重都将替换为检查点的权重。
仅训练一些层
迁移学习的另一个重要部分是冻结不需要训练的层的权重,同时允许对某些层(通常是最后一层)进行训练。 在 TensorFlow 中,我们可以仅将要优化的层传递给求解器(在此示例中,仅将 FC 层传递给):
代码语言:javascript复制train_step = tf.train.AdamOptimizer(learning_rate).minimize(loss, var_list=list_fc_linear)
完整代码
在此示例中,我们将从 MNIST 卷积自编码器示例中加载权重。 我们将仅恢复编码器部分的权重,冻结卷积层,并训练 FC 层以执行数字分类:
代码语言:javascript复制import tensorflow as tf
import numpy as np
import os
from models import CAE_CNN_Encoder
SAVE_FOLDER='/tmp/cae_cnn_transfer'
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("MNIST_data/", one_hot=True)
model = CAE_CNN_Encoder(latent_size = 20)
model_in = model.input
model_out = model.output
labels_in = model.labels
# Get all convs weights
list_convs = [v for v in tf.global_variables() if "conv" in v.name]
# Get fc1 and logits
list_fc_layers = [v for v in tf.global_variables() if "fc" in v.name or "logits" in v.name]
# Define the saver object to load only the conv variables
saver_load_autoencoder = tf.train.Saver(var_list=list_convs)
# Define saver object to save all the variables during training
saver = tf.train.Saver()
# Define loss for classification
with tf.name_scope("LOSS"):
loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits_v2(logits=model_out, labels=labels_in))
correct_prediction = tf.equal(tf.argmax(model_out,1), tf.argmax(labels_in,1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
# Solver configuration
with tf.name_scope("Solver"):
train_step = tf.train.AdamOptimizer(1e-4).minimize(loss, var_list=list_fc_layers)
# Initialize variables
init = tf.global_variables_initializer()
# Avoid allocating the whole memory
gpu_options = tf.GPUOptions(per_process_gpu_memory_fraction=0.200)
sess = tf.Session(config=tf.ConfigProto(gpu_options=gpu_options))
sess.run(init)
# Restore only the CONV weights (From AutoEncoder)
saver_load_autoencoder.restore(sess, "/tmp/cae_cnn/model.ckpt-34")
# Add some tensors to observe on tensorboad
tf.summary.image("input_image", model.image_in, 4)
tf.summary.scalar("loss", loss)
merged_summary = tf.summary.merge_all()
writer = tf.summary.FileWriter(SAVE_FOLDER)
writer.add_graph(sess.graph)
#####Train######
num_epoch = 200
batch_size = 10
for epoch in range(num_epoch):
for i in range(int(mnist.train.num_examples / batch_size)):
# Get batch of 50 images
batch = mnist.train.next_batch(batch_size)
# Dump summary
if i % 5000 == 0:
# Other summaries
s = sess.run(merged_summary, feed_dict={model_in:batch[0], labels_in:batch[1]})
writer.add_summary(s,i)
# Train actually here (Also get loss value)
_, val_loss, t_acc = sess.run((train_step, loss, accuracy), feed_dict={model_in:batch[0], labels_in:batch[1]})
print('Epoch: %d/%d loss:%d' % (epoch, num_epoch, val_loss))
print('Save model:', epoch)
saver.save(sess, os.path.join(SAVE_FOLDER, "model.ckpt"), epoch)
总结
在本章中,我们学习了如何,何时以及为什么使用迁移学习。 这被认为是一个非常强大的工具,因为它使我们能够使用从其他领域学到的功能来以较少的数据很好地概括。 我们看了一些示例,现在应该清楚如何在自己的任务中实现迁移学习。
在下一章中,我们将看到如何组织我们的数据以及如何扩展 CNN 架构,以构建准确而实用的机器学习系统。
八、机器学习最佳实践和故障排除
在机器学习工程中,至关重要的是要知道如何在系统开发过程中进行操作,以避免陷阱并解决常见问题。 创建机器学习系统(最节省时间和金钱)的最简单方法是重用已应用于您自己的类似问题的代码和预先训练的模型。 如果这不能满足您的需求,那么您可能需要训练自己的 CNN 体系结构,因为有时这可能是解决问题的最佳方法。 但是,面临的最大挑战之一是找到针对您的问题量身定制的大规模,公开可用的数据集。 因此,通常情况下,您可能需要创建自己的数据集。 创建自己的数据集时,至关重要的是适当组织它,以确保成功进行模型训练。
在本章中,我们将介绍并讨论日常工作流程,这些工作流程将帮助您回答以下问题:
- 我应该如何分割数据集?
- 我的数据集足以代表我的问题吗?
- 我的模型应该有多复杂才能有效且准确?
- 评估模型的最佳方法是什么?
- 我应该如何构造我的代码?
建立机器学习系统
为了构建机器学习系统,建议从一个新的小项目开始并逐步改进它:
- 查找与您类似的问题并下载代码(并测试模型以检查结果)
- 根据需要找到扩展计算的方法(即 AWS/Google Cloud)
- 从较小的数据集开始,以避免浪费时间等待一个周期
- 从简单的架构开始
- 使用可视化/调试(例如,TensorBoard)
- 微调模型,微调超参数,深度,架构,层和损失函数
- 扩展数据集并确保其尽可能干净
- 将您的数据集分为训练,开发和测试集
- 评估模型
数据准备
所有机器学习算法的骨干都是数据。 机器学习算法学习的一切都来自数据。 因此,至关重要的是向算法提供代表问题陈述的正确数据。 就像已经看到的那样,深度学习特别需要大量数据用于训练模型。 有时我们可以说一定数量的数据足以解决问题,但是却永远不够! 多多益善。 能够正确训练的模型的复杂度与训练模型上的数据量成正比。 有限的数据将为该问题的模型架构选择设定上限。 在考虑可用数据量时,还值得注意的是,其中一部分也需要用于验证和测试目的。
下一节将讨论数据分区及其对任何机器学习任务进度的重要性。
训练/开发/测试集的划分
在讨论将数据划分到其中之前,让我们定义“训练集”,“开发集”和“测试集”。
- 训练集:用于训练机器学习算法的数据/示例集。 在机器学习中,此数据用于查找模型/分类器的“最佳”权重。 通常,使用的大多数数据都进入训练集。
- 开发/验证集:用于在训练的中间阶段评估模型/分类器的数据部分。 该集合用于微调超参数和评估具有各种配置的模型架构。 它在模型开发期间使用,而不是在最终模型评估中使用。
- 测试集:模型经过微调和充分训练(我们对训练/开发集的损失感到满意),我们认为它已完全训练。 然后评估该模型。 对其进行评估的数据称为测试集。 测试集由数据的看不见部分组成,因此提供了最终模型表现的无偏估计。
为了获得高表现的神经网络,将数据集正确划分为训练集,开发集和测试集非常重要。 它有助于更快地迭代。 另外,它允许更有效地测量算法的偏差和方差,以便我们可以选择有效方式进行改进的方法。
在以前的周期,我们拥有较小的数据集(例如最多 10,000 个示例)和简单的分类器,我们会将数据集拆分为训练和测试集。 通常将训练集分成较小的集,以使用称为交叉验证的技术来训练分类器。 优良作法是按 60/20/20 的比例拆分数据集(即 60% 的训练数据,20% 的开发数据,20% 的测试数据)。 但是,大数据的现代周期已经改变了这一经验法则。 如果我们有 1,000,000 个示例,则拆分比例已更改为 98/1/1(即 98% 的训练数据,1% 的开发数据,1% 的测试数据)。
随着我们拥有更多的数据,开发和测试集的比例将变小。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FsxapwAY-1681568428349)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/6b559cef-7224-4438-b1d1-48dcb61c69d5.png)]
开发和测试集不匹配
除了拆分数据之外,数据的分布还对神经网络的表现产生巨大影响。 应用深度学习中的大多数问题来自开发人员和测试集数据分布的不匹配。 我们需要记住,开发和测试数据应该来自类似的分发。 例如,如果我们以如下方式收集和分割人员检测数据,即从网页上收集人员的训练图像,而使用移动电话收集测试集图像,则会出现分布不匹配的情况。 这里的问题是,在训练模型时,我们会根据其在开发数据上的表现来微调网络的参数和架构,如果开发数据与训练数据相似且与测试数据不同,则与训练集相比开发数据中存在很高的偏差。 在开发集上获得良好的评估结果并不一定意味着该模型可以很好地推广。 在这种情况下,对分布完全不同的集合进行测试可能会导致不良结果。 这是浪费时间和精力。 解决方案是首先合并开发集和测试集,随机将它们洗牌,最后将洗过的数据再次拆分为开发集和测试集。 这有助于在将机器学习算法成功训练到最终应用方面取得更快的进展。
何时更改开发/测试集
根据评估指标对开发/测试集执行良好但不满足客户要求(即在部署时执行不佳)的算法,表明我们在数据集中缺少正确的目标数据。 在这种情况下,我们需要对数据集进行更改,因为它对于目标应用而言不够代表性。 考虑对猫图像进行分类。 如果训练/开发/测试集使用的是高分辨率,高质量的图像(姿势完美的猫),而目标应用正在查看具有不同视角的猫或运动中的图像(模糊),则可以期望算法在部署时表现不佳。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xBxSnuAK-1681568428349)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/d33ade2f-7c16-473c-974d-7ba1a204b47d.png)]
偏差和方差
如第 2 章,“深度学习和卷积神经网络”中所讨论的,方差和偏差分别表示过拟合和欠拟合。 我们可以使用训练集,开发集和测试集误差来诊断“欠拟合”和“过拟合”的问题。
考虑以下场景,其中我们的数据来自两个不同的分布,分别称为分布 1 和分布 2。分布 2 表示我们关心的目标应用。 问题是,我们如何在这种分布上定义训练,开发和测试集。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HRVivGIw-1681568428350)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/00d0d711-3009-4542-bb6e-3988f1007b50.png)]
最好的方法是根据上图将其拆分。 分布 1 被拆分为训练集,其一部分用作开发集。 在这里,我们称其为“训练开发集”(因为开发集与训练集具有相同的分布)。 分布 1 主要用于训练,因为它是一个大型数据集。 分布 2 分为测试集和开发集,它们与分布 1 中的任一集无关。这里要强调的一点是,测试和开发集应来自同一发行版,并且属于我们实际上关心的应用,即目标应用。 开发集和测试集通常是小的数据集,因为它们的目的是给出模型/算法的无偏表现估计。
模型在不同数据集分区上的误差差异,以及查看人为误差可为我们提供诊断偏差和方差问题的见解
下表显示了当左列中的集合之间存在误差时,应如何诊断。 注意,人为水平误差是此分析的基准,它为比较模型提供了基准。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LZ1kie7m-1681568428350)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/43b92390-d3d8-4104-a0e8-0b0e03958557.png)]
下表可以更好地说明这一点。 在这些示例中,我们假设在所有情况下的最佳/人为误差均最小,即 1%。 通常,深度学习模型的准确率与人类相似,因此将其作为比较可帮助您找到良好的架构。
- 高偏差/欠拟合
人为/最佳误差 | 1% |
训练误差 | 15% |
与人员水平的表现相比,训练误差较大,这意味着该模型甚至无法拟合数据。 训练有素,因此欠拟合/高偏差。 但是,当我们在这种情况下查看开发误差时,它可以很好地概括,因此不会丢失所有内容。
- 高方差/过拟合
训练误差 | 1.5% |
训练开发误差 | 30% |
在这种情况下,该模型在看不见的数据上表现不佳,该数据与训练集属于同一分布,但不是训练的一部分。 这意味着该模型无法概括,因此会过拟合训练数据。
- 高方差和高偏差
| 训练误差 | 20% | | 训练开发误差 | 40% |
这种情况是最坏的情况,因为我们观察到该模型无法正确拟合训练数据,并且不能很好地概括。 这可以通过更改模型架构来解决。
- 数据不匹配
训练开发误差 | 2% |
开发误差 | 15% |
当模型很好地适合来自与训练集相同分布的开发集,并且对来自不同分布的开发集表现不佳时,这会导致数据不匹配问题,如本章前面所述。
- 过拟合开发集
开发误差 | 2% |
测试误差 | 15% |
下图以流程图的形式介绍了解决上述问题的解决方案/指南:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nyHkPnHC-1681568428350)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/641fa640-9e3d-40c0-b72f-027ecc050fff.png)]
ML 基本秘籍
有用的图表说明了测试和训练误差如何随模型复杂性而变化,如下所示。 一方面,当模型过于复杂时,往往会过拟合训练数据,因此,训练误差减小而测试误差增大。 另一方面,较简单的模型往往不适合并且无法推广。 模型复杂度的理想范围是在“测试误差”开始增加之前以及“训练误差”接近零时的某个位置。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ej49zgGo-1681568428350)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/3b7eb077-6c15-4a77-989a-40b12e4b70db.png)]
数据不平衡
我们已经看到了数据表示和分布在解决偏差和方差问题中的重要性。 我们遇到的另一个相关问题是分类任务中各个类之间的数据分配不均。 这称为数据不平衡。 例如,如果我们有一个二分类问题,并且其中一个类别有 50000 张图像,而另一个类别只有 1000 张图像,这可能会导致训练算法的表现出现巨大问题。 我们必须通过以下方法解决数据不平衡的问题:
收集更多数据
是的,最好使类数据分布相等。 收集尽可能多的数据,并用较少的样本填充类。 为此,您可以在互联网上搜索与您的问题相似的数据库并将其包括在内。 简单的网络搜索还可以带来许多由各种来源上传的图像。 有时您会发现,使用更多数据不会提高模型表现。 这表明您的模型可能已达到极限。
查看您的效果指标
分类准确率不是一个很好的衡量标准,尤其是当我们的数据不平衡时。 这种准确率将更倾向于具有更多数据的类。 有许多良好的表现评估指标可以真实地描述算法的执行方式,例如混淆矩阵,受试者工作特性曲线(ROC),精确召回(PR)曲线和 F1 分数。 这些将在本章稍后详细说明。
数据综合/增强
在无法从其他资源收集数据,数据集太小或收集的数据不能很好地表示的情况下,我们需要以某种方式自行生成数据。 这称为数据增强。 智能生成的数据可以解决许多问题,包括数据集不平衡,训练数据不足和过拟合。
数据扩充通常是作为输入数据流水线的一部分来完成的,该流水线在训练时为模型提供数据。 随机地,而不是提供原始训练图像,您将应用一些增强来更改它。 有很多方法可以进行数据扩充,但是一些示例是:
- 增加噪音
- 应用几何变换
- 交换颜色通道
- 随机颜色扰动
- 调整亮度/对比度/色相
- 只需添加类似于网络存在问题的增强,例如,您的模型不适用于黑白图像; 只需将其添加为新的扩充
重采样数据
这是关于改变我们建立训练批次的方式。 我们通过更改选择特定类别的可能性来做到这一点。 例如,如果您有两个类 A 和 B,其中我们的 A 实例比 B 多得多,则可以更改采样系统以选择比 A 多的 B。
损失函数加权
我们还可以处理不平衡类,并通过将损失权重包括在内来处理不平衡数据的分类问题。 这种惩罚或权重迫使模型更多地关注少数群体(样本较少的类别)。 在前面的章节中讨论过的惩罚型 SVM 和焦点损失检测器算法就是这样的例子。
Tensorflow 已经具有其损失函数,并内置了加权选项:
tf.losses.sparse_softmax_cross_entropy(labels=label, logits=logits, weights=weights)
Tf.nn.weighted_cross_entropy_with_logits
例如,如果您尝试对 A,B,C 三个类别进行分类,其中 A 为 10%,B 为 45%,C 为 45%,则可以将tf.losses.sparse_softmax_cross_entropy
用于以下权重:[1.0, 0.3, 0.3]
。
评价指标
在为模型选择评估指标时,我们还需要小心。 假设对于狗/非狗分类问题,我们有两种算法的准确率分别为 98% 和 96%。 乍一看,这些算法看起来都具有相似的表现。 让我们记住,分类准确率定义为做出的正确预测数除以做出的预测总数。 换句话说,真阳性(TP)和真阴性(TN)预测数除以预测总数。 但是,可能出现的情况是,随着狗图像的出现,我们还会得到大量被错误分类为狗的背景或类似外观的对象,通常称为假阳性(FP)。 另一个不良行为可能是许多狗图像被错误分类为负面或假阴性(FN)。 显然,根据定义,分类准确率不能捕获误报或误报的概念。 因此,需要更好的评估指标。
第一步,我们将构建一个混淆矩阵,该矩阵总结最后显示的段落:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GK1YwTha-1681568428351)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/645c3d53-fa90-4fd7-8d1e-51fd5188aa5f.png)]
根据此表,我们可以定义四个其他指标,这些指标可以使我们更好地了解已实现的结果。 这些是:
- 真阳性率(TPR)或灵敏度或召回率:当对象存在时测试结果为阳性的概率(真实阳性率,以百分比表示),
= TP / (TP FN)
- 假阳性率(FPR):是特定测试错误拒绝实际阴性的概率,
= FP / (FP TN)
- 阳性预测值(PPV)或精度:当检测结果为阳性(以百分比表示)时该对象存在的概率,
= TP / (TP FP)
- 阴性预测值(NPV):测试阴性(表示为百分比)时不存在对象的概率,
= TN / (TN FN)
为了更好地了解这些指标的实用性,我们以两种不同算法的以下两个混淆矩阵为例,并计算前面的指标。
范例 1:
正 | 负 | ||
---|---|---|---|
预测为正 | 10(TP) | 13(FP) | 23 |
预测为负 | 75(FN) | 188(TN) | 263 |
85 | 201 | 286 |
ACC: (TP TN) / (TP TN FP FN) = 198/286 = 0.69
TPR: TP / (TP FN) = 10/85 = 0.11
FPR: FP / (FP TN) = 13 / 201 = 0.06
PPV: TP / (TP FP) = 10/23 = 0.43
NPV: TN / (TN FN) = 188/263 = 0.71
范例 2:
正 | 负 | ||
---|---|---|---|
预测为正 | 0(TP) | 0(FP) | 0 |
预测为负 | 85(FN) | 201(TN) | 286 |
85 | 201 | 286 |
ACC: (TP TN) / (TP TN FP FN) = 201/286 = 0.70
TPR: TP / (TP FN) = 0/85 = 0
FPR: FP / (FP TN) = 0 / 201 = 0
PPV: TP / (TP FP) = 0/0 = 0
NPV: TN / (TN FN) = 201/286 = 0.70
在第一个示例中,我们可以得到 69% 的正确精度,但是在第二个示例中,通过仅对每个示例进行预测,我们实际上将我们的精度提高到 70% ! 显然,仅预测所有事物为负类的模型并不是一个很好的模型,这就是我们所说的准确率悖论。 简单来说,“准确率悖论”说,即使模型可能具有更高的准确率,但实际上可能并不是更好的模型。
如前面的示例中所示,当类别不平衡变大时,更可能发生这种现象。 鼓励读者对包含 85 个正样本和 85 个负样本的平衡数据集重复上述测试。 如果假阳性与假阴性的比例与前面的示例相同,则这将导致第一个示例的分类准确率为 52%,第二个示例的分类准确率为 50%,这表明准确率悖论不适用于平衡数据集。
为了能够正确评估算法,我们需要查看其他评估指标,例如 TPR 和 FPR。 我们可以看到在第二个示例中它们都为零,这表明算法根本无法检测到所需的正向对象。
使用精度度量的不平衡数据集的另一种情况是癌症测试,其中生病的人数大大少于健康的人数。 以下是为此解决的示例。
生病 | 健康 | 总数 | |
---|---|---|---|
测试结果阳性 | 99(TP) | 999(FP) | 1,098 |
测试结果阴性 | 1(FN) | 98,901(TN) | 98,902 |
总数 | 100 | 99,900 | 100,000 |
ACC: (TP TN) / (TP TN FP FN) = 0.99
TPR: TP / (TP FN) = 0.99
FPR: FP / (FP TN) = 0.01
PPV: TP / (TP FP) = 0.09
NPV: TN / (TN FN) = 0.99
此处的测试似乎表现不错,因为准确率为 99%。 但是,如果您被诊断出患有癌症,这并不意味着您患该病的可能性为 99%。 应该注意的是,在 1098 个测试阳性的患者中,只有 99 个患有该疾病。 这意味着,如果您获得了阳性测试,那么对于准确率高达 99% 的测试,您实际患病的可能性仅为 9%。
这些示例很好地警告了我们的目标是在测试数据中进行均衡分配,尤其是当您使用准确率指标比较不同模型的有效性时。
比较不同算法的其他有用方法是精确调用和接收器操作特性曲线。 如果我们针对不同的阈值计算上述指标,则可以绘制这些图。 如果我们算法的输出不是二进制的(0 表示负数,1 表示正数),但分数在测试为正时接近 1,而在测试为负时接近零,那么 TP,TN,FP,FN 的数量将取决于我们选择的阈值。
让我们以图像中猫检测为例。 对于每个区域,分类器输出一个分数,该分数显示出它对检测的信心。 如果将阈值设置为 0.5,则 0.6 的分数表示检测为阳性,而 0.45 的分数表示阴性。 如果阈值降低到 0.4,则两次检测都将变为阳性。 下表说明了前面的指标随阈值而变化的情况。
阈值 | FPR | TPR | PPV | TP | TN | FN | FP |
---|---|---|---|---|---|---|---|
0.72 | 1 | 0.98 | 0.33 | 487 | 0 | 7 | 990 |
0.88 | 0.5 | 0.97 | 0.46 | 485 | 430 | 9 | 560 |
0.97 | 0.1 | 0.94 | 0.8 | 464 | 878 | 30 | 112 |
0.99 | 0.05 | 0.93 | 0.87 | 460 | 923 | 34 | 67 |
1.06 | 0.01 | 0.87 | 0.96 | 430 | 976 | 64 | 14 |
1.08 | 0.005 | 0.84 | 0.98 | 416 | 985 | 78 | 5 |
1.16 | 0.001 | 0.69 | 0.99 | 344 | 989 | 150 | 1 |
如果将 FPR 与 TPR 作图,我们将得到所谓的 ROC(受试者工作特性)曲线,如下所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vqxxlYFm-1681568428351)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/a38d1883-1a58-4a94-9a77-67facda77864.png)]
要获得精确召回(PR)曲线,我们需要针对精确度/ PPV 绘制召回率/ TPR。 下图显示了该曲线的示例。 建议读者进一步研究如何解释 ROC 和 PR 曲线。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lca3Rrks-1681568428351)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/6db1bd89-3d34-4435-980d-ef49716d192f.png)]
代码结构最佳实践
在前面的章节中,我们将张量流图封装到一个类中,而无需进一步讨论。 这个想法本身已经是很好的编码实践。 有一个类负责构建图并仅公开对使用模型有用的东西(即输入/输出)是一种很好的编程习惯,可以节省大量时间。
单例模式
使用设计模式来解决一些软件设计问题也是一种常见的做法。 python 中最简单,最有用的设计模式之一就是单例模式。 当您只想将一个类的实例强制仅用于一个对象时,可以使用它,因此,即使您在项目中的多个不同位置多次实例化该类,也将引用同一个对象。 在我们的情况下,如果我们要求 TensorFlow 创建具有相同名称的多个节点或图,则会引发错误。 因此,我们在创建图使用单例模式,以避免生成两次。
在下面的示例中,我们总结了一个简单的分类模型,同时还确保不会多次构建图(也称为单例模式)。
注意__new__
类方法的定义。 在 Python 中,当我们创建一个类的新实例时,将调用__new__
。
class CAE_CNN_Encoder(object):
__instance = None
# Singleton pattern
def __new__(cls):
if CAE_CNN_Encoder.__instance is None:
# First time new is called
CAE_CNN_Encoder.__instance = object.__new__(cls)
CAE_CNN_Encoder.__instance.build_graph()
return CAE_CNN_Encoder.__instance
def build_graph(self, img_size=28):
self.__x = tf.placeholder(tf.float32, shape=[None, img_size * img_size], name='IMAGE_IN')
self.__x_image = tf.reshape(self.__x, [-1, img_size, img_size, 1])
self.__y_ = tf.placeholder("float", shape=[None, 10], name='Y')
with tf.name_scope('ENCODER'):
##### ENCODER
# CONV1: Input 28x28x1 after CONV 5x5 P:2 S:2 H_out: 1 (28 4-5)/2 = 14, W_out= 1 (28 4-5)/2 = 14
self.__conv1_act = tf.layers.conv2d(inputs=self.__x_image, strides=(2, 2), name='conv1',
filters=16, kernel_size=[5, 5], padding="same", activation=tf.nn.relu)
# CONV2: Input 14x14x16 after CONV 5x5 P:0 S:2 H_out: 1 (14 4-5)/2 = 7, W_out= 1 (14 4-5)/2 = 7
self.__conv2_act = tf.layers.conv2d(inputs=self.__conv1_act, strides=(2, 2), name='conv2',
filters=32, kernel_size=[5, 5], padding="same", activation=tf.nn.relu)
with tf.name_scope('LATENT'):
# Reshape: Input 7x7x32 after [7x7x32]
self.__enc_out = tf.layers.flatten(self.__conv2_act, name='flatten_conv2')
self.__dense = tf.layers.dense(inputs=self.__enc_out, units=200, activation=tf.nn.relu, name='fc1')
self.__logits = tf.layers.dense(inputs=self.__dense, units=10, name='logits')
def __init__(self, img_size=28):
if CAE_CNN_Encoder.__instance is None:
self.build_graph(img_size)
@property
def output(self):
return self.__logits
@property
def labels(self):
return self.__y_
@property
def input(self):
return self.__x
@property
def image_in(self):
return self.__x_image
CNN 创建秘籍
以下几点基于我们在训练神经网络方面的经验以及该领域研究人员认为的当前最佳实践。 希望如果您需要从头开始设计自己的 CNN 架构,他们将为您提供帮助。 但是,在尝试设计自己的 CNN 之前,您应该查看其他现成的架构以从中学习,并检查它们是否已经为您完成了工作。
- 使用内核大小为
3x3
的卷积层。 就参数和计算而言,较大的内核更昂贵。 最重要的是,如我们在前面的章节中所看到的,您可以堆叠卷积层以产生更大的感受域,并受益于更多的非线性激活。 - 第一层卷积通常应至少具有 32 个过滤器。 这样,更深的层不受第一层提取的特征数量的限制。
- 尽可能避免使用池化层。 相反,请使用步长为 2 的卷积层。这将像池化那样对输入进行下采样,但它不会像池化那样丢弃宝贵的信息。 同样,使用跨步卷积就像将卷积和合并在一层中一样。
- 减小特征图的空间大小时,应增加使用的过滤器数量,以免丢失过多信息。 在深度网络中,请避免在第一层中过快减小空间大小。
- 请遵循本章中有关从小规模开始网络设计,然后逐渐增加复杂性的建议,以避免出现过大的问题。
- 使用批量规范化。 确实有助于训练您的网络!
- 随着您对网络的深入了解,逐渐减小特征图的空间大小。
- 最小化 FC 层的数量(在最后一层之前使用丢弃)。 仅在最终需要连接某些标量特征时才使用 FC。 (您甚至可以通过在输入通道上进行编码来避免这种情况)
- 如果您需要较大的感受域(对象大小接近总图像大小的检测或分类),请尝试对每层使用具有指数膨胀因子的膨胀卷积。 这样,您将在保持少量参数的同时非常迅速地扩大接收范围。
- 如果网络变深并且训练损失没有减少,请考虑使用剩余连接。
- 在使网络精度在期望值之内并且如果计算成本成为问题之后,您可能会根据使用情况,研究深度卷积,瓶颈模块之类的技术,或现场出现的任何技术。
请记住,CNN 的训练和设计是一门经验丰富的科学,因此请始终注意,被视为最佳实践的内容会迅速发生变化。
总结
在本章中,我们了解到遵循最佳实践将对作为机器学习工程师的日常活动有所帮助。 我们已经看到了如何准备数据集并将其拆分为子集,以促进对网络的正确训练和微调。 此外,我们还研究了执行有意义的测试,其中获得的结果代表了将模型部署到目标应用时所看到的结果。 涉及的另一个主题是对数据的过拟合和不足,以及为了解决这些问题而应遵循的最佳实践。 此外,解决了数据集不平衡的问题,我们已经看到了一个简单的示例,该示例可能在哪里找到(疾病诊断)。 为了解决这个问题,建议收集更多的数据,扩充数据集并选择不平衡数据集不变的评估指标。 最后,展示了如何构造代码以使其更具可读性和重用性。
在下一章中,我们将看到如何管理大型数据集以及如何将训练过程扩展到多个 GPU 和系统。
九、大规模训练
到目前为止,在本书中,我们使用或查看的数据集的大小从数万个(MNIST)样本到略超过一百万个(ImageNet)。 尽管所有这些数据集在刚推出时都被认为是巨大的,并且需要使用最先进的机器,但是 GPU 和云计算等技术的迅捷发展现已使它们易于训练。 由功率较低的机器的人。
但是,深度神经网络的强大功能来自其随输入的数据量进行扩展的能力。 简而言之,这意味着您可以用来训练模型的数据越好,越干净,结果越好。 研究人员已经意识到了这一点,我们可以看到,新的公共数据集中的训练样本数量一直在增加。
结果,很有可能,如果您开始研究行业中的问题,甚至只是最近的 Kaggle 竞赛,您很有可能将使用可能包含数百万个元素的数据集。 如何处理如此庞大的数据集,以及如何有效地训练模型,就成为一个现实问题。 差异可能意味着要等待三天而不是 1 个月的时间来完成模型的训练,因此这不是您想出错的事情。
在本章中,您将学习一些解决以下问题的方法:
- 数据集太大而无法放入内存
- 如何在多台机器上扩展训练
- 数据过于复杂而无法在普通目录文件夹和子文件夹中进行组织
在 TFRecords 中存储数据
让我们从训练网络进行图像分类的示例开始。 在这种情况下,我们的数据将是带有相关标签的图像集合。 我们存储数据的一种方法是在类似目录的文件夹结构中。 对于每个标签,我们将有一个文件夹,其中包含该标签的图像:
代码语言:javascript复制-Data
- Person
-im1.png
- Cat
-im2.png
- Dog
-im3.png
尽管这似乎是存储数据的一种简单方法,但一旦数据集大小变得太大,它就会具有一些主要缺点。 当我们开始加载它时,一个很大的缺点就来了。
打开文件是一项耗时的操作,必须多次打开数百万个文件,这会增加大量的训练时间开销。 最重要的是,由于我们已将所有数据拆分开,因此不会将其存储在一个漂亮的内存块中。 硬盘驱动器将不得不做更多的工作来尝试查找和访问所有硬盘。
解决办法是什么? 我们将它们全部放入一个文件中。 这样做的好处是,您的所有数据将在计算机内存中更好地对齐以便读取,这将加快处理速度。 将所有内容都保存在一个文件中也意味着我们不必花费时间来加载数百万个文件,这将非常缓慢且效率低下。
我们可以根据需要使用几种不同的格式来存储数据,例如 HDF5 或 LMDB。 但是,当我们使用 TensorFlow 时,我们将继续使用其自己的内置格式 TFRecords。 TFRecords 是 TensorFlow 自己的标准文件格式,用于存储数据。 它是一种二进制文件格式,提供对其内容的顺序访问。 它足够灵活,可以存储复杂的数据集和标签以及我们可能想要的任何元数据。
创建 TFRecord
在开始之前,让我们分解一下 TFRecord 的工作方式。 打开 TFRecord 文件进行写入后,创建一个称为Example
的内容。 这只是一个协议缓冲区,我们将使用它填充要保存在其中的所有数据。 在示例中,我们将数据存储在Feature
中。 功能是描述示例中数据的一种方式。 功能可以是以下三种类型之一:字节列表,浮点列表或int64
列表。 将所有数据放入功能部件并将它们写入示例缓冲区后,我们会将整个协议缓冲区序列化为字符串,然后将其写入 TFRecord 文件。
让我们看看这在实践中如何工作。 我们将继续使用前面的图像分类示例,并创建一个 TFRecord 来存储相关数据。
首先,我们创建文件,这还将返回给我们一种写入文件的方式:
代码语言:javascript复制writer = tf.python_io.TFRecordWriter('/data/dataset.tfrecord')
接下来,我们假设图像已经加载并且已经作为numpy
数组存储在内存中; 我们将在以后看到如何存储编码图像:
# labels is a list of integer labels.
# image_data is an NxHxWxC numpy array of images
for index in range(len(labels)):
image_raw = image_data[index, ...].tobytes()
# Create our feature.
my_features= {
'image_raw': tf.train.Feature(bytes_list=tf.train.BytesList(value=[image_raw])), 'label':
tf.train.Feature(int64_list=tf.train.Int64List(value=[labels[index]]))}
# The Example protocol buffer.
example = tf.train.Example(features=tf.train.Features(feature=my_features)
writer.write(example.SerializeToString())
writer.close() # Close our tfrecord file after finishing writing to it.
我们遍历标签列表,将每个图像数组一次转换为原始字节。
要在示例中存储数据,我们需要向其添加功能。 我们将功能存储在字典中,其中每个键都是我们选择的某些字符串名称,例如label
,值是tf.train.Feature
,这就是我们的数据。
必须使用tf.train.BytesList
,tf.train.Int64List
或tf.train.FloatList
将进入tf.train.Feature
的数据转换为期望的正确类型。
接下来,我们创建一个tf.train.Example
协议缓冲区并将功能传递给它。 最后,我们将Example
序列化为字符串并将其写入 TFRecord 文件。 一旦遍历了整个图像数组,就必须记住关闭文件进行写入。
存储编码图像
优化内存使用率的一种方法是使用某种压缩方式(即 PNG)对图像进行编码,在这种情况下 TFRecord 会更小,但是您仍需要在使用之前解压缩数据,这可能需要一些时间。 在实践中要做的是使用另一个 CPU 内核来减轻计算量。
分片
尽管我们说最好将所有数据保存在一个文件中,但实际上并非 100% 正确。 由于 TFRecords 是按顺序读取的,因此,如果仅使用一个文件,我们将无法重新整理数据集。 经过一段时间的训练之后,每次到达 TFRecord 的末尾时,您都将返回到数据集的开头,但是不幸的是,每次浏览文件时,数据的顺序都相同。
为了允许我们随机播放数据,我们可以做的一件事是通过创建多个 TFRecord 文件并将数据散布到这些多个文件中来分片我们的数据。 这样,我们可以在每个周期处重新整理加载 TFRecord 文件的顺序,因此我们在训练时将为我们有效地整理数据。 每 100 万张图像需要 1000 个碎片,这是可以遵循的良好基准。
在下一节中,我们将看到如何使用 TFRecords 建立有效的数据馈送流水线。
建立高效的流水线
当我们处理较小的数据集时,仅将整个数据集加载到计算机内存中就足够了。 如果您的数据集足够小,这很简单并且可以正常工作; 但是,在很多时候,情况并非如此。 现在我们将研究如何克服这个问题。
为了避免一次加载所有数据,我们将需要创建一个数据流水线以将我们的训练数据馈入模型。 除其他事项外,该流水线将负责从存储中加载一批元素,对数据进行预处理,最后将数据提供给我们的模型。 幸运的是,这一切都可以使用 TensorFlow 数据 API 轻松完成。
对于这些示例,我们将假定已将数据保存到多个(在本例中为两个)TFRecord 文件中,如先前所述。 如果您有两个以上,则没有区别; 您只需在设置内容时包括所有名称即可。
我们首先从所有 TFRecord 文件名的列表创建 TFRecord 数据集:
代码语言:javascript复制 # Create a TFRecord dataset that reads all of the Examples from
two files.
train_filenames= ["/data/train1.tfrecord", "/data/train2.tfrecord"]
train_dataset = tf.data.TFRecordDataset(filenames)
接下来,我们必须解码 TFRecords。 为此,我们编写了一个函数,该函数将接受 TFRecord,对其进行解码,然后返回输入图像及其对应的标签:
代码语言:javascript复制# Function for decoding our TFRecord. We assume our images are fixed size 224x224x3
def decode_tfrec(proto_in):
my_features = {'image_raw': tf.FixedLenFeature([], tf.string),
'Label': tf.FixedLenFeature([], tf.int64)}
parsed_features = tf.parse_single_example(proto_in, features=my_features)image = tf.decode_raw(parsed_features['image_raw'], tf.uint8)
image = tf.cast(image, tf.float32) # Tensorflow data needs to be float32.
image = tf.reshape(image, [224,224,3]) # Need to reshape your images.
label = tf.cast(parsed_features['label'], tf.int32) # Labels need to be int32
label = tf.one_hot(label, depth=...) # Convert our labels to one hot.
return image, label
然后,我们将此函数传递给dataset.map()
方法,该方法将为我们执行:
train_dataset = train_dataset.map(decode_tfrec, num_parallel_calls=4)
映射转换的并行调用
默认情况下,您在数据集上调用的任何映射转换都仅作用于数据集的单个元素,并且将按顺序处理元素。 要加快速度并使用所有 CPU 功能,最简单的方法是将num_parallel_calls
参数设置为可用的 CPU 内核数。 这样,我们就不会浪费任何可用的 CPU 能力。 但是,警告您不要将其设置为高于可用内核的数量,因为由于调度效率低下,这实际上可能会降低性能。
您想要对数据进行的任何转换(例如数据扩充)也可以编写为函数,然后像以前一样传递给map
方法,以将其应用于数据集。 例如,请注意以下代码:
train_dataset = train_dataset.map(decode_tfrec, num_parallel_calls=4) # Decode tfrecord.
train_dataset = train_dataset.map(data_augmentation,
num_parallel_calls=4) # Augment data.
批量
您希望在流水线末尾做的最后一件事是生成一批准备发送到 GPU 进行训练的数据。 这可以通过批量方法简单地完成,并传入所需的批量大小:
代码语言:javascript复制train_dataset = train_dataset.batch(128) # Take a batch of 128 from the dataset.
当试图使我们的流水线尽可能高效时,批次的大小是一个重要的参数。 尽可能大可能并不总是最好的。 例如,如果您的图像上有很多预处理步骤,那么当 CPU 对大量图像进行预处理时,GPU 可能会处于空闲状态。
预取
我们能够建立有效数据流水线的另一种方法是始终准备好一批数据准备发送到 GPU。 理想情况下,在训练模型时,我们希望 GPU 的使用率始终保持在 100%。 这样,我们可以最大程度地利用昂贵的硬件,该硬件可以在训练时有效地计算前进和后退的传球次数。
为此,我们需要 CPU 加载并准备一批图像,以准备在向前和向后传递模型的过程中传递给 GPU。 幸运的是,在收集批量之后,我们可以使用简单的预取转换轻松完成此操作,如下所示:
代码语言:javascript复制train_dataset= train_dataset.batch(128).prefetch(1)
使用预取将确保我们的数据流水线在进行训练时为我们准备一整批数据,准备将其加载到 GPU 中以进行下一次迭代。 这样做可以确保我们的流水线在等待一批批次收集之前不会减慢速度,并且如果获取批次所需的时间少于模型的前后传递时间,那么我们的流水线将尽可能高效。
要清楚的是,此处使用prefetch(1)
表示我们prefetch
整批数据。 这就是为什么我们将批量作为流水线的最后一步,并在此处使用预取功能,因为这样做最有效。
追踪图
TensorFlow 提供了一种很好的方式来分析并查看整个图通过其时间轴跟踪工具执行所需的时间。 这是查看图的哪些部分正在减慢训练速度并发现数据流水线中任何低效率的好工具。
我们将从为您提供如何跟踪图的示例开始。 这非常简单:您只需在常规代码中添加几行,就会生成一个 JSON 文件,我们可以将该文件加载到 Google Chrome 浏览器中,以查看图执行的所有时间:
代码语言:javascript复制from tensorflow.python.client import timeline
.... # Your model and training code here
with tf.Session() as sess:
# We set some options to give to the session so graph execution is profiled.
options = tf.RunOptions(trace_level=tf.RunOptions.FULL_TRACE)
run_metadata = tf.RunMetadata()
# Run your graph and supply the options we set.
sess.run(model_output, options=options, run_metadata=run_metadata)
# We create the Timeline object here then write it to json file.
created_timeline = timeline.Timeline(run_metadata.step_stats)
chome_readable_trace = created_timeline.generate_chrome_trace_format()
with open('my_timeline.json', 'w') as file:
file.write(chome_readable_trace)
在此代码中,我们导入 TensorFlow 时间轴模块,然后设置两个选项以启用图跟踪并将其提供给Session.run()
。 运行图之后,我们创建Timeline
对象,该对象将包含对图执行进行性能分析的结果。 然后,我们将其转换为 Chrome 跟踪格式,最后将其写入 JSON 文件。
要查看结果,您需要打开一个新的 Chrome 窗口。 然后,在地址栏中输入chrome://tracing
并按Enter
。 左上角将有一个加载按钮。 使用它来加载刚刚保存的 JSON 文件。
现在将显示跟踪图的结果。 查看此内容将告诉您图的每个部分执行所需的时间。 您应该特别注意存在大块空白的地方。 这些空白表示设备(例如您的 GPU)正坐在那里等待数据,以便它们可以执行计算。 您应该尝试通过优化数据馈送方式来消除这些问题。
但是请注意,您的流水线可能已完全优化,但是您没有 CPU 周期来足够快地处理流水线。 检查您的 CPU 使用情况,看看是否是这种情况。
TensorFlow 中的分布式计算
在本节中,您将学习如何在 TensorFlow 中分配计算; 强调如何做到这一点的重要性如下:
- 并行运行更多实验(即,找到超参数,例如网格搜索)
- 在多个 GPU(在多个服务器上)上分配模型训练,以减少训练时间
一个著名的用例是,Facebook 发布了一篇论文,该论文能够在 1 小时(而不是几周)内训练 ImageNet。 基本上,它在 256 个 GPU 上的 ImageNet 上训练了 ResNet-50,该 GPU 分布在 32 台服务器上,批量大小为 8,192 张图像。
模型/数据并行
实现并行性和在多台服务器中扩展任务的方法主要有两种:
- 模型并行性:当模型不适合 GPU 时,您需要在不同服务器上计算层。
- 数据并行性:当我们在不同的服务器上分布相同的模型但处理不同的批次时,每个服务器将具有不同的梯度,并且我们需要在服务器之间进行某种同步。
在本节中,我们将重点介绍易于实现的数据并行性:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dyuhPlMs-1681568428351)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/d768ce6b-5760-4e1e-87e6-4ac7904b55be.png)]
同步/异步 SGD
如前所述,在数据并行性中,每个模型都会从训练集中获取一些数据并计算自己的梯度,但是考虑到每个工作器都将拥有相同的模型,我们需要在更新模型之前以某种方式进行同步。
在同步 SGD 中,所有工作器都会计算一个梯度并等待计算所有梯度,然后将模型更新并再次分发给所有工作器:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CyLmD8lp-1681568428351)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/f8da6b99-2533-4df2-87c9-2ba3c2165494.png)]
当数据不适合在一台计算机上时
可能出现的一个问题是,我们根本无法将数据存储在一台计算机上和/或我们仍然需要在该数据集上进行搜索。 为了解决此类问题,我们可能需要分布式 NoSQL 数据库,例如 Cassandra。 Cassandra 支持在可用性和性能至关重要的多个系统上进行数据分发:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Fybcdq8J-1681568428352)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/fbc95570-cec1-4bbb-b2a1-04cd9aca41bd.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V4rna5v0-1681568428352)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/39ffcbf8-2596-4a24-8a6b-2c4b661edaa2.png)]
卡桑德拉(Cassandra)尽最大努力避免出现单点故障。 例如,所有节点都将像一种主节点一样工作(没有实际的主节点),因此,在某种类型的高可用性备份中,所有节点都有责任处理请求并自动在节点之间分配数据。
NoSQL 系统的优势
与关系数据库(例如旧版本的 MySQL 和 PostgreSQL)相比,NoSQL 数据库在数据量太大以及不需要关系数据库的功能(例如触发器或存储过程)时会发光。 。
在继续之前,让我们列出 NoSQL 系统的优点:
- 水平缩放; 要获得更高的性能,只需添加更多机器
- 我们不需要事先知道表之间的关系
- 允许在整个工作期间更改表结构
- 更快(没有复杂的关系数据库机制)
- 数据通常保存在分布式文件系统上,因此,例如,将图像存储在 NoSQL 数据库中就可以了
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-75iDHnRT-1681568428352)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/d618b557-f396-4253-8a9c-6b1805c45120.png)]
安装 Cassandra(Ubuntu 16.04)
安装 Oracle Java 1.8:
sudo apt-get update
sudo add-apt-repository ppa:webupd8team/java
sudo apt-get update
sudo apt-get -y install oracle-java8-installer
安装 Cassandra:
echo "deb http://www.apache.org/dist/Cassandra/debian 310x main" | sudo tee -a /etc/apt/sources.list.d/cassandra.sources.list
curl https://www.apache.org/dist/Cassandra/KEYS | sudo apt-key add -
sudo apt-get update
sudo apt-get install cassandra
sudo service cassandra status
sudo nodetool status
CQLSH 工具
CQLSH 是允许您向 Cassandra 节点发出 SQL 命令的工具:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-95ujQmRA-1681568428352)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/6bce090b-199b-46e8-a34a-9e2e3e4d6f4e.jpg)]
对于图形用户界面,有一个很好的工具叫做 DBWeaver,它也可以完成此工作:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HzLEfYIt-1681568428353)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/0f086a20-5dfe-4e60-9def-f15577d50f49.png)]
DBWeaver 示例
创建数据库,表和索引
- 首先,我们需要创建数据库(键空间)并选择节点如何复制数据:
CREATE KEYSPACE mydb WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 };
- 现在,我们创建一个表:
CREATE TABLE tb_drive ( id uuid PRIMARY KEY, wheel_angle float, acc float, image blob );
- 如下添加一些数据:
INSERT INTO tb_drive (id,wheel_angle,acc) VALUES (now(),0.2,0.5);
INSERT INTO tb_drive (id,wheel_angle,acc) VALUES (now(),0.1,0.5);
INSERT INTO tb_drive (id,wheel_angle,acc) VALUES (now(),0.0,0.5);
- (在任何时间点)创建要查询的所有列的索引(这就是为什么要快)
CREATE INDEX idxAngle ON tb_drive (wheel_angle);
CREATE INDEX idxAcc ON tb_drive (acc);
将 Python 用于查询
首先,在开始玩之前,我们需要安装 Python 驱动程序pip install cassandra-driver
; 以下代码片段仅列出了 Cassandra 集群中表的内容:
from cassandra.cluster import Cluster
import cassandra.util
import uuid
import numpy as np
# Considering that the cluster is on localhost
cluster = Cluster()
# Other option if you know the IPs
# cluster = Cluster(['192.168.0.1', '192.168.0.2'])
# Get a session to the database
session = cluster.connect('mydb')
# Doing a query
rows = session.execute('SELECT * FROM tb_drive limit 5')
print('Columns:',rows.column_names)
for row in rows:
print(row.id, row.acc, row.wheel_angle)
在 Python 中填充表格
在以下示例中,我们将填充表格,包括一个存储图像的字段:
代码语言:javascript复制insert_string = """INSERT INTO tb_drive (id, wheel_angle, acc, image) VALUES (%s, %s, %s, %s)"""
for data in dataset:
# Split from dataset the image path, steering angle, and acceleration
img_path, steering_angle, acc = data
# Load image (png compressed)
with open(img_path, 'rb') as f:
content_file = f.read()
# Insert into database
session.execute(insert_string,(uuid.uuid1(), steering_angle, acc, content_file))
做备份
对于备份(快照):结果存储在var/lib/cassandra/data/)
中:
nodetool -h localhost snapshot mydb
要还原数据(可能需要截断/删除表),请执行以下操作:
然后我们复制在目录/var/lib/Cassandra/data/keyspace/table_name-UUID
之前创建的快照(数据库备份文件),然后:
nodetool refresh
在云中扩展计算
在您作为机器学习工程师的设计周期和生命周期中,您可能会遇到这样的情况,即您办公室中可用的计算能力根本不够,并且您不能等待 IT 团队为您购买新服务器。 因此,例如,如果您能负担得起每小时 24.48 美元的价格,则可能拥有一个 p3.16xlarge,带有 8 个 GPU Nvidia V100、64 核和 488 GB RAM。
在本部分中,您将学习有关可帮助您解决计算能力不足问题的 Amazon AWS 服务的信息。
您将了解以下 Amazon Cloud Services:
- 弹性计算云(EC2)
- S3
- SageMaker
EC2
这是我们创建服务器的服务,您基本上可以在其中创建任何服务器来完成工作:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i2sKMiuk-1681568428353)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/9289b8b0-dd5e-414d-92ed-6d86b641fdbe.png)]
在这里,您可以配置诸如访问服务器的方式(通常使用私钥):
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uh0fR84i-1681568428353)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/932ef3e1-6117-4d2d-ae95-364bd962c0f2.png)]
在这里,我们配置所需的磁盘空间:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kvCnqGKL-1681568428353)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/02449f1f-b4a5-474b-b101-b945c1050b8f.png)]
在这里,我们配置将可用的端口:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h1gatytN-1681568428353)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/889e9ca5-5aec-4f15-ba71-5835459a79dc.png)]
可用端口
AMI
这是 AWS 中最酷的功能之一,它使您可以从所有数据和已安装的工具(从一个服务器实例到另一个服务器实例)创建映像。 因此,您可以仅使用所有工具,驱动程序等配置一台服务器,以后再使用具有相同映像的另一台服务器:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a1ZuYSsP-1681568428354)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/c1d35dc4-78cb-492e-b80e-bb18f768cc69.png)]
储存(S3)
Amazon S3 是存储系统,您可以在其中从常规 HTTP 请求上传/下载文件。 S3 的构想是存储桶,您可以从中存储/下载文件。 另外,有些插件可让您将 S3 直接映射到您的 EC2 实例,例如某些远程文件夹(不建议使用):
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8FWhOU8p-1681568428354)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/b80eb463-7348-4012-a829-cf7cc4e310ab.png)]
以下屏幕截图显示了如何创建存储桶:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CbaBMp7x-1681568428354)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/f9fc72c1-0b23-4c61-bf05-a8e941efa887.png)]
S3 系统可以进行公共配置,因此人们可以从任何地方下载/上传内容。
SageMaker
SageMaker 提供了一种在云中训练/部署机器学习模型的简便方法。 SageMaker 将提供 Jupyter 笔记本,您可以在其中可视化/训练模型并直接连接到 S3 中的数据(提供了访问 S3 的 API):
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pGaTQMju-1681568428354)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/267b6c45-039c-4978-a801-025f9f562e00.png)]
在这里,我们显示了创建笔记本实例的默认选项:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F6Sc4TCb-1681568428354)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/111c47f0-b5d3-4192-a4e8-9f15918348bf.png)]
这是笔记本面板:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lXEmP8i6-1681568428355)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/34fd1923-47ac-424f-b30b-4e4602f4bac3.png)]
并且,这是笔记本(您可以查看训练模型的示例来检查 API 的工作方式):
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TZAj0kQH-1681568428355)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-cnn-tf/img/5a8a5836-b47a-4691-afff-476fa5e867c8.png)]
总结
在本章中,您学习了如何处理数据集太大而无法由普通台式计算机处理的数据。 我们看到了如何在多个 GPU 和机器之间训练 TensorFlow 模型,最后,我们研究了用于存储数据并将其有效地馈送到模型的最佳实践。
在本书的学习过程中,我们研究了计算机视觉中当前流行的许多问题,以及如何使用深度学习解决所有这些问题。 我们还提供了有关如何在 TensorFlow 中实现这些功能的见解。 在此过程中,我们介绍了如何使用 TensorFlow。