卷积神经网络stride(TensorFlow 卷积神经网络实用指南:6~10)
原文:Hands-On Convolutional Neural Networks with TensorFlow
协议:CC BY-NC-SA 4.0
译者:飞龙
本文来自【ApacheCN 深度学习 译文集】 ,采用译后编辑(MTPE)流程来尽可能提升效率 。
不要担心自己的形象 ,只关心如何实现目标 。——《原则》 ,生活原则 2.3.c
六 、自编码器 ,变分自编码器和生成对抗网络
本章将介绍一种与到目前为止所看到的模型稍有不同的模型。 到目前为止提供的所有模型都属于一种称为判别模型的模型 。 判别模型旨在找到不同类别之间的界限 。 他们对找到P(Y|X)-给定某些输入X的输出Y的概率感兴趣 。 这是用于分类的自然概率分布 ,因为您通常要在给定一些输入X的情况下找到标签Y 。
但是 ,还有另一种类型的模型称为生成模型 。 建立了生成模型以对不同类的分布进行建模 。 他们对找到P(Y,X)-输出Y和输入X一起出现的概率分布感兴趣 。 从理论上讲 ,如果您可以捕获数据中类别的概率分布 ,则将了解更多信息 ,并且可以使用贝叶斯规则来计算P(Y|X) 。
生成模型属于无监督学习算法的类别 。 无监督意味着我们不需要标签数据。
本章列出了一些我们将要学习的关键主题:
自编码器 变分自编码器 生成对抗网络 实现各种生成模型来生成手写数字为什么是生成模型
在下图中,我们可以看到生成模型和判别模型之间的主要区别 。 使用判别模型 ,我们通常尝试找到在数据中不同类别之间进行区分或“区分 ”的方法 。 但是 ,使用生成模型,我们尝试找出数据的概率分布。 在图示中 ,分布由包含较小圆圈的蓝色和黄色大斑点表示 。 如果我们从数据中学到这种分布 ,我们将能够采样或“生成 ”应该属于它的新数据点,例如红色三角形 。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OXPtb5Wl-1681568428339)(https://cdn.yuucn.cn/wp-content/uploads/2023/04/1682036435-2b52160e715de12.png)]
尝试捕获数据集的概率分布具有以下用例:
使用未标记的数据预训练模型 扩展数据集(理论上 ,如果您捕获数据的概率分布 ,则可以生成更多数据) 压缩数据(有损) 创建某种模拟器(例如 ,可以通过四个输入来控制四轴飞行器;如果捕获此数据并在其上训练生成模型 ,则可以学习无人机的动态)使用生成模型时的期望是 ,如果我们能够创建类似于原始输入数据的新数据 ,则我们的模型必须了解一些有关数据分布的知识。
训练了生成神经网络模型 ,以产生类似于训练集的数据样本 。 由于模型参数的数量小于训练数据的维数 ,因此迫使模型发现有效的数据表示形式 。
自编码器
我们将要看到的第一个生成模型是自编码器模型 。 自编码器是一个简单的神经网络 ,由两部分组成:编码器和解码器 。 这个想法是编码器部分会将您的输入压缩到较小的尺寸 。 然后,从这个较小的维度尝试使用模型的解码器部分重建输入 。 通常用许多名称来称呼这种较小的尺寸 ,例如潜在空间 ,隐藏空间,嵌入或编码 。
如果自编码器能够再现其输入 ,则从理论上讲 ,该潜在空间应该对表示原始数据所需的所有重要信息进行编码,但其优点是尺寸小于输入 。 编码器可以被认为是一种压缩输入数据的方式 ,而解码器是一种将其解压缩的方式 。 在下图中 ,我们可以看到一个简单的自编码器的外观。 我们的潜在空间或编码是中间标记为z的部分 。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-j80GKKKM-1681568428340)(https://cdn.yuucn.cn/wp-content/uploads/2023/04/1682036441-2b52160e715de12.png)]
传统上 ,在自编码器中 ,构成网络的层只是全连接层 ,但是通过使用卷积层 ,自编码器也可以扩展到图像 。 与之前一样 ,编码器会将输入图像压缩为较小的表示形式 ,而解码器将尽最大努力恢复信息。 区别在于 ,编码器现在是将数据压缩为特征向量的 CNN,而不是具有全连接层的 ANN ,并且解码器将使用转置的卷积层从编码中重新生成图像 。
此处提供了一个自编码器处理图像的示例 。 对于解码器部分
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rMm9PB4S-1681568428341)(https://cdn.yuucn.cn/wp-content/uploads/2023/04/1682036447-2b52160e715de12.png)]
对于任何自编码器 ,损失函数都会引导编码器和解码器重建输入。 使用的常见损失是自编码器的输出与网络输入之间的 L2 损失 。 我们现在应该问自己一个问题:“使用 L2 损失比较图像是一个好主意吗? ” 。 如果您拍摄以下图像,即使它们看起来截然不同 ,它们实际上彼此之间的距离L2也相同:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wnHmf2Oo-1681568428341)(https://cdn.yuucn.cn/wp-content/uploads/2023/04/1682036454-2b52160e715de12.png)]
这表明当您使用 L2 损失比较图像时 ,并非总是可以依靠它,因此在使用它时应牢记这一点 。
卷积自编码器示例
以下 TensorFlow 代码将为 MNIST 数据集构建卷积自编码器模型 。 代码的第一部分将构建模型 ,编码器和解码器的图 。 在代码中 ,我们突出显示模型的一部分 ,其输出将是我们的潜在向量:
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])与卷积自编码器损失有关的代码段如下:
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://cdn.yuucn.cn/wp-content/uploads/2023/04/1682036460-2b52160e715de12.png)]
定义正态分布的参数
我们需要两个参数来跟踪并强制我们的 VAE 模型在潜在空间中产生正态分布:
平均值(应为零) 标准差(应为 1)在下图中 ,我们给出了具有不同均值和标准差值的正态分布示例 。 仅使用这两个值 ,我们就可以产生一个正态分布 ,可以从中采样:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1j10WS88-1681568428342)(https://cdn.yuucn.cn/wp-content/uploads/2023/04/1682036466-2b52160e715de12.jpg)]
VAE 损失函数
在 VAE 中,损失函数由两部分组成:
生成损失:此损失将模型输出与模型输入进行比较 。 这可能是我们在自编码器中使用的损失 ,例如 L2 损失 。 潜在损失:此损失将潜在向量与零均值 ,单位方差高斯分布进行比较 。 我们在这里使用的损失将是 KL 散度损失 。 如果 VAE 开始产生不是来自所需分布的潜在向量,则该损失项将对 VAE 造成不利影响。以下屏幕截图显示了 VAE 的损失 ,它是生成损失和潜在空间损失的组合:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FBjwqyd2-1681568428342)(https://cdn.yuucn.cn/wp-content/uploads/2023/04/1682036472-2b52160e715de12.jpg)]
Kullback-Leibler 散度
KL 散度损失将产生一个数字 ,该数字指示两个分布彼此之间的接近程度 。
两个分布之间的距离越近,损失就越低 。 在下图中 ,蓝色分布正在尝试对绿色分布进行建模。 随着蓝色分布越来越接近绿色分布 ,KL 散度损失将接近于零 。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-40md4kmb-1681568428342)(https://cdn.yuucn.cn/wp-content/uploads/2023/04/1682036479-2b52160e715de12.png)]
训练 VAE
为了训练 VAE 并使用 KL 散度损失 ,我们首先需要研究如何生成潜向量 。 我们将使编码器产生两个向量 ,而不是让编码器直接精确地产生一个潜在向量。 第一个是平均值的向量μ ,第二个是标准差值的向量σ 。 根据这些 ,我们可以创建第三个向量 ,其中使用μ和σ从高斯分布中采样元素向量的第i个值作为该高斯分布的均值和标准差 。 然后 ,该第三采样向量被发送到解码器 。
现在 ,我们的模型如下所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DAaiSTrQ-1681568428342)(https://cdn.yuucn.cn/wp-content/uploads/2023/04/1682036485-2b52160e715de12.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://cdn.yuucn.cn/wp-content/uploads/2023/04/1682036492-2b52160e715de12.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ygxK1ADL-1681568428343)(https://cdn.yuucn.cn/wp-content/uploads/2023/04/1682036498-2b52160e715de12.jpg)]
产生的潜向量将与以前相同 ,但是现在进行此更改可以使梯度流回到 VAE 的编码器部分 。 下图显示了在进行重新参数化之前的 VAE 模型 ,在左侧进行了重新参数化之后 。 蓝色框是损失函数的两个部分。 查看该图 ,您可以看到我们的梯度现在可以向后流动 ,因为我们不再具有红色框(示例节点)来挡路了:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8dCi90AV-1681568428343)(https://cdn.yuucn.cn/wp-content/uploads/2023/04/1682036504-2b52160e715de12.png)]
这是 TensorFlow 中的重新参数化形式:
# 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 的一部分 ,该部分负责使用我们之前的新重新参数化技巧来创建潜在向量。 我们添加了对最终潜在向量的记录 ,以检查它是否按照我们期望的那样遵循单位高斯分布产生向量:
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)之后,我们添加网络的解码器部分 ,该部分包括一个全连接层 ,然后是两个转置的卷积层:
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 。 然后 ,我们可以将这种损失传递给我们选择的优化器 ,以创建我们的训练步骤:
# 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 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 图的代码 ,如下所示:
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://cdn.yuucn.cn/wp-content/uploads/2023/04/1682036511-2b52160e715de12.png)]
判别器和生成器都将具有自己的损失函数 ,但是它们的损失都相互依赖 。
让我们总结一下 GAN 模型的两个主要模块或网络:
生成器:使用大小为 N 的一维向量作为输入 ,创建类似于真实图像数据集的图像(选择 N 取决于我们) 判别器:验证提供给它的图像是真实的还是伪造的GAN 的一些实际用法如下:
使用判别器网络权重作为不同任务的初始化,类似于我们对自编码器可以执行的操作 使用生成器网络创建新图像 ,可能会扩大您的数据集 ,就像我们可以使用经过训练的 VAE 解码器一样 将判别器用作损失函数(对于图像 ,可能优于 L1/L2) ,并且也可以在 VAE 中使用 通过将生成的数据与标记的数据混合来进行半监督学习现在我们将向您展示如何在 TensorFlow 中实现非常简单的 GAN 。 一旦经过训练 ,我们的 GAN 的生成器部分就可以用于根据 100 个长随机噪声向量创建 MNIST 手写数字。 让我们开始吧!
判别器
我们需要做的第一件事就是创建我们的判别网络 。 为此 ,我们将几个全连接层堆叠在一起 。 判别器将 784 个长度向量作为输入 ,这是我们的28x28 MNIST 图像变平。 每个图像的输出将只是一个数字 ,这是鉴别者对该图像为真实图像的信心程度的分数 。 我们使用 Leaky ReLu 作为激活函数 ,以防止 ReLu 单元死亡 。
我们返回原始对率,因为损失函数将为我们应用 Sigmoid 激活函数 ,以确保判别器输出在 0 到 1 之间:
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://cdn.yuucn.cn/wp-content/uploads/2023/04/1682036517-2b52160e715de12.png)]
在这里 ,D是我们的判别器 ,G是我们的生成器 ,z是输入到生成器的随机向量 ,x是真实图像 。 尽管我们在此处给出了 GAN 损失的总和 ,但实际上更容易分别考虑这两种优化 。
为了训练 GAN ,我们将在判别器和生成器之间交替进行梯度步骤更新。 在更新判别器时,我们要尝试使最大化判别器做出正确选择的概率 。 在更新生成器时 ,我们想尝试使最小化判别器做出正确选择的可能性 。
但是 ,为了实际实现,我们将与之前给出的内容相比 ,稍微改变 GAN 损失函数; 这样做是为了帮助训练收敛。 变化是 ,当更新生成器时,而不是最小化判别器做出正确选择的可能性; 我们改为最大化判别器做出错误选择的概率:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HdeKTBmK-1681568428344)(https://cdn.yuucn.cn/wp-content/uploads/2023/04/1682036524-2b52160e715de12.png)]
更新判别器时 ,我们尝试使最大化 ,它对真实数据和伪数据均做出正确选择的可能性:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9RNqAkB7-1681568428344)(https://cdn.yuucn.cn/wp-content/uploads/2023/04/1682036530-2b52160e715de12.png)]
生成器损失
生成器想要欺骗判别器 ,换句话说 ,使判别器输出q用于生成的图像G(z) 。 生成器损失只是施加到生成器结果的判别器输出的二项式交叉熵损失的负值 。 请注意 ,由于生成器始终尝试生成“真实 ”图像 ,因此交叉熵损失可简化为:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P37yG78P-1681568428345)(https://cdn.yuucn.cn/wp-content/uploads/2023/04/1682036536-2b52160e715de12.png)]
在这里 ,每项的含义如下:
m:批量 D:判别器 G:生成器 z:随机噪声向量我们想在训练 GAN 时最大化损失函数。 当损失最大化时 ,这意味着生成器能够生成可能使判别器蒙蔽的图像 ,并且判别器针对生成的图像输出 1 。
判别器损失
鉴别者希望能够区分真实图像和生成图像 。 它想为真实图像输出 1,为生成图像输出 0 。 判别器损失函数具有以下公式 ,由于 GAN 训练和标记的工作方式而再次简化:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IOkxZdfo-1681568428345)(https://cdn.yuucn.cn/wp-content/uploads/2023/04/1682036542-2b52160e715de12.png)]
此损失函数有两项:
应用于判别器模型的二项式交叉熵产生了一些真实数据x 将二项式交叉熵应用于所生成数据G(z)的判别器模型结果如前所述 ,我们采取这些不利条件,并希望在训练 GAN 时最大化此损失函数 。 当这种损失最大化时 ,这意味着判别器能够区分实际输出和生成的输出 。 注意 ,当判别器对于真实图像输出 1 而对于所生成图像输出 0 时,该损失最大 。
综合损失
在 TensorFlow 中 ,我们可以实现整个 GAN 损失 ,如以下代码所示 。 作为输入 ,我们从判别器的输出中获取来自生成器的一批伪图像和来自我们的数据集的一批真实图像:
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)然后 ,我们将一批真实图像和一批生成的样本提供给判别器 。 我们在这里使用变量范围来重用我们的模型变量 ,并确保不创建第二个图:
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 )由于需要分别更新它们 ,因此我们将判别器和生成器的权重分开:
discriminator_vars = tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES, discriminator) generator_vars = tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES, generator)最后,我们计算损失并将其与相关权重一起发送给优化器以进行更新:
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://cdn.yuucn.cn/wp-content/uploads/2023/04/1682036549-2b52160e715de12.png)]
深度卷积 GAN
深度卷积 GAN(DCGAN)是我们之前看到的普通 GAN 的扩展 。 我们不是使用全连接层 ,而是使用卷积层 。 想法是使用卷积层可以帮助生成器形成更好的图像 。 以下是这种模型的示例:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fOV1443a-1681568428345)(https://cdn.yuucn.cn/wp-content/uploads/2023/04/1682036555-2b52160e715de12.jpg)]
DCGAN 的示例实现与之前训练普通 GAN 相同,只是简单地将判别器和生成器网络换成一些卷积架构 ,如以下代码所示 。 请注意 ,生成器将使用转置卷积来上采样:
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 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://cdn.yuucn.cn/wp-content/uploads/2023/04/1682036561-2b52160e715de12.png)]
现在产生的质量非常好 ,几乎与真实数据没有区别 。 另外 ,请注意 ,图像确实非常清晰 ,并且没有像我们之前那样模糊且几乎没有伪影。
需要注意的几点是:
对于判别器:再次使用泄漏的 relu ,不要使用最大池 。 仅使用跨步卷积或平均池 。 对于生成器:在最后一层使用 relu 和 tanh。 通常 ,最佳实践是在生成器和判别器上都使用批量规范化层 。 它们将始终设置为训练模式 。 有时,人们运行生成器优化器的次数是运行判别器优化器的两倍。这是一个简单的 DCGAN 在生成人脸图像时可以达到的质量的示例:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LMBWFXSd-1681568428346)(https://cdn.yuucn.cn/wp-content/uploads/2023/04/1682036567-2b52160e715de12.jpg)]
WGAN
Wasserstein GAN 是 GAN 的另一种变体 ,它解决了训练 GAN 时可能发生的问题 ,即模式崩溃 。 此外,其目的在于给出一种度量 ,该度量指示 GAN 何时收敛 ,换句话说,损失函数具有该值的含义 。
重要的更改是从损失中删除对数并修剪判别器权重 。
此外 ,请按照下列步骤操作:
训练判别器比生成器更多 减少判别器的权重 使用 RMSProp 代替 Adam 使用低学习率(0.0005)WGAN 的缺点是训练起来较慢:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nndLLplk-1681568428346)(https://cdn.yuucn.cn/wp-content/uploads/2023/04/1682036574-2b52160e715de12.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Az3UoKBB-1681568428346)(https://cdn.yuucn.cn/wp-content/uploads/2023/04/1682036580-2b52160e715de12.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2EPEKR10-1681568428347)(https://cdn.yuucn.cn/wp-content/uploads/2023/04/1682036586-2b52160e715de12.png)]
WGAN 产生的图像结果仍然不是很好 ,但是该模型确实有助于解决模式崩溃问题 。
BEGAN
BEGAN 的主要思想是在判别器上使用自编码器 ,这将有其自身的损失 ,该损失会衡量自编码器对某些图像(生成器或真实数据)的重构程度:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TYW29cnP-1681568428347)(https://cdn.yuucn.cn/wp-content/uploads/2023/04/1682036592-2b52160e715de12.jpg)]
BEGAN 的一些优点如下:
高分辨率(128x128)人脸生成(2017 最新技术) 。 提供一种衡量收敛的方法 。 即使没有批量规范和丢弃法也有良好的结果 。 超参数可控制世代多样性与质量 。 更高的质量也意味着更多的模式崩溃 。 不需要两个单独的优化器。这是 BEGAN 在生成人脸任务时可以生成的图像质量的示例:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-STysAwmE-1681568428347)(https://cdn.yuucn.cn/wp-content/uploads/2023/04/1682036599-2b52160e715de12.jpg)]
条件 GAN
条件 GAN 是普通 GAN 的扩展 ,其中判别器和生成器都被设置为某些特定类别y 。这具有有趣的应用 ,因为您可以将生成器固定到特定的类 ,然后使其生成我们选择的特定同一类的多个不同版本 。 例如 ,如果将y设置为与 MNIST 中的数字 7 对应的标签 ,则生成器将仅生成 7 的图像。
使用条件 GAN,minimax 游戏变为:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9DmNG3Zu-1681568428347)(https://cdn.yuucn.cn/wp-content/uploads/2023/04/1682036605-2b52160e715de12.png)]
在这里 ,我们依赖于额外输入y ,它是输入图像的类标签 。
合并x和y,或z和y的最简单方法是将它们连接在一起 ,这样我们的输入向量就更长 。 这将创建一个更加受控制的数据集扩充系统。 这是 TensorFlow 代码中的一个示例:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2ZPsiHOf-1681568428347)(https://cdn.yuucn.cn/wp-content/uploads/2023/04/1682036611-2b52160e715de12.jpg)]
GAN 的问题
GAN 当前最大的问题是 ,它们很难训练 。 幸运的是,有一些技术可以使事情变得容易 ,这是目前非常活跃的研究领域 。
在这里 ,我们将介绍训练 GAN 的一些问题以及如何解决它们 。
损失可解释性
训练 GAN 时的问题之一是 ,生成器损失和判别器损失的值都没有明显的影响 。 这不像训练分类器 ,只是等待损失下降以查看模型是否在训练 。
对于 GAN ,损失值的下降并不一定意味着该模型正在训练中:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LKC6hpQM-1681568428348)(https://cdn.yuucn.cn/wp-content/uploads/2023/04/1682036618-2b52160e715de12.jpg)]
通过许多人的实验和研究 ,以下是有关如何使用 GAN 损失值的一些提示:
您不希望判别器的损失下降得很快 ,因为它将无法向生成器提供反馈以改善它 。 如果生成器损失迅速下降 ,则意味着它发现了一个判别器弱点 ,并一次又一次地利用了这一弱点 。 如果发生这种情况,则称为模式折叠 。损失实际上仅对查看训练中是否出现问题有好处 。 因此 ,没有很好的方法知道训练已经收敛。 通常 ,最好的办法是继续查看生成器的输出 。 确保输出看起来与您的期望接近,并且输出种类丰富 。
模式崩溃
这可能是您在训练 GAN 时遇到的第一个问题。 当生成器找到一组特定的输入来欺骗判别器时 ,就会发生模式崩溃 ,并且它会继续利用这种故障情况并将潜伏Z空间中的许多值折叠为相同的值 。
解决此问题的一种方法是使用“小批量功能 ”或“展开 GANs ”,或者完全停止训练 ,然后在生成器开始创建非常狭窄的输出分布时重新开始:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XEW6ZvWt-1681568428348)(https://cdn.yuucn.cn/wp-content/uploads/2023/04/1682036624-2b52160e715de12.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ckB4fURc-1681568428348)(https://cdn.yuucn.cn/wp-content/uploads/2023/04/1682036630-2b52160e715de12.jpg)]
改善 GAN 的可训练性的技术
在这里 ,我们将介绍一些在训练 GAN 时使生活更轻松的技术:
归一化输入到 -1/1 之间 使用 BatchNorm 使用 Leaky Relu(判别器) 在生成器输出上使用 Relu(生成器) ,tanh 对于下采样 ,请使用平均池化或跨步卷积 使用 Adam 优化器 如果判别器损失迅速下降 ,则说明存在问题 在生成器上使用压降(在训练阶段和测试阶段)小批量判别器
用于改善模式崩溃的一些技术如下:
取得判别器某层的输出 将判别器的输入重塑为矩阵 计算 L1 距离 计算 L1 距离的指数和 将结果连接到输入(判别器的某些层)[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MTwnh6eo-1681568428348)(https://cdn.yuucn.cn/wp-content/uploads/2023/04/1682036637-2b52160e715de12.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uHMKNFwo-1681568428349)(https://cdn.yuucn.cn/wp-content/uploads/2023/04/1682036643-2b52160e715de12.jpg)]
总结
在本章中 ,我们了解了生成模型及其与判别模型的不同之处 。 我们还讨论了各种自编码器 ,包括深度 ,变体和卷积。 此外 ,我们了解了一种新型的生成模型,称为生成对抗网络(GAN) 。 在了解了所有这些生成模型之后 ,我们看到了如何在 TensorFlow 中自己训练它们以生成手写数字 ,并看到了它们可以产生的不同质量的图像 。
在第 7 章,“迁移学习 ”中 ,我们将学习迁移学习及其如何帮助我们加快训练速度 。
七 、迁移学习
迁移学习的作用恰如其名 。 这个想法是将从一项任务中学到的东西迁移到另一项任务上 。 为什么? 实际上 ,每次都从头开始训练整个模型的效率很低,其成功取决于许多因素 。 另一个重要原因是 ,对于某些应用 ,公开可用的数据集不够大 ,无法训练出像 AlexNet 或 ResNet 这样的深层架构而又不会过拟合 ,这意味着无法推广 。 示例应用可以从用户给出的一些示例中进行在线学习 ,也可以是细粒度的分类 ,其中类别之间的差异很小 。
一个非常有趣的观察结果是 ,由于您冻结了所有其余部分(无论是检测还是分类) ,最终的层可以用于完成不同的任务 ,最终权重看起来非常相似 。
这导致了迁移学习的想法。 这意味着在大量数据上训练的深度架构(例如 ImageNet)可以很好地概括化,以至于其卷积权重可以充当特征提取器 ,类似于常规的视觉表示 ,并且可以用于为各种任务训练线性分类器 。
本章旨在教读者如何在 TensorFlow 中采用现成的训练有素的模型,更改其结构以及为特定任务重新训练某些层 。 我们将看到迁移学习将如何帮助改善结果并加快训练时间。
本章涵盖的主要主题如下:
使用来自另一个训练过的模型的权重预先初始化一个模型 在需要时使用 TensorFlow 加载模型并冻结/解冻层什么时候?
研究表明 ,在 ImageNet 上训练的卷积网络权重中的特征提取优于常规特征提取方法 ,例如 SURF,可变形部分描述符(DPD) ,直方图定向梯度(HOG)和词袋(BoW) 。 这意味着无论常规视觉表示如何工作 ,卷积特征都可以同样好地使用 ,唯一的缺点是更深的架构可能需要更长的时间来提取特征 。
当在 ImageNet 上训练深层卷积神经网络时 ,第一层中的卷积过滤器的可视化(请参见下图)显示 ,他们学习了低层特征 ,类似于边检测过滤器 ,而卷积过滤器在最后一层学习高级功能 ,这些功能捕获特定于类的信息。 因此 ,如果我们在第一个池化层之后提取 ImageNet 的特征并将其嵌入 2D 空间(例如,使用 t-SNE) ,则可视化将显示数据中存在一些无中心状态 ,而如果在全连接层上执行相同操作,我们将注意到具有相同语义信息的数据被组织成簇 。 这意味着网络可以在更高层次上很好地概括 ,并且有可能将这种知识迁移到看不见的类别中 。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kk0992Ab-1681568428349)(https://cdn.yuucn.cn/wp-content/uploads/2023/04/1682036649-2b52160e715de12.png)]
根据对与 ImageNet 相似度较小的数据集进行的实验 ,在以下任务上,基于 ImageNet 训练的基于卷积神经网络权重的特征比常规特征提取方法的表现更好:
对象识别:此 CNN 特征提取器可以成功地对其他类别不可见的数据集执行分类任务 。 域适应:这是当训练和测试数据来自不同的分布 ,而标签和类别数相同时 。 不同的域可以考虑使用不同的设备或在不同的设置和环境条件下捕获的图像 。 具有 CNN 功能的线性分类器可以在不同域中成功地将具有相同语义信息的图像聚类 ,而 SURF 功能则可以针对特定领域的特征进行过拟合 。 精细分类:这是我们要在同一高级类中的子类之间进行分类的时候 。 例如 ,我们可以对鸟类进行分类 。 尽管没有对细粒度数据进行训练 ,但 CNN 功能以及逻辑回归功能的表现优于基线方法 。 场景识别:在这里 ,我们需要对整个图像的场景进行分类。 在对象分类数据库上经过训练的 CNN 特征提取器 ,顶部带有一个简单的线性分类器 ,其表现优于应用于识别数据的传统特征提取器的复杂学习算法 。这里提到的某些任务与图像分类没有直接关系 ,图像分类是 ImageNet 训练的主要目标 ,因此有人希望 CNN 功能无法推广到看不见的场景 。 但是,这些功能与简单的线性分类器相结合 ,表现优于手工制作的功能。 这意味着 CNN 的学习权重是可重用的 。
那么什么时候应该使用迁移学习呢? 当我们有一个任务时 ,由于问题的性质,可用数据集很小(例如对蚂蚁/蜜蜂进行分类) 。 在这种情况下 ,我们可以在包含相似语义信息的较大数据集上训练我们的模型 ,然后用较小的数据集仅训练最后一层(线性分类器)。 如果我们只有足够的可用数据,并且有一个更大的相似数据集 ,则对该相似数据集进行预训练可能会导致模型更健壮 。 通常情况下 ,我们使用随机初始化的权重来训练模型 ,在这种情况下 ,将使用在其他数据集上训练过的权重来初始化模型 。 这将有助于网络更快地融合并更好地推广 。 在这种情况下 ,仅微调模型顶端的几层是有意义的 。
经验法则是 ,从网络顶部开始 ,可用数据越多 ,可以训练的层就越多 。 通过预训练(例如在 ImageNet 上)模型初始化模型权重 。
怎么样? 概述
我们应该如何使用转学? 有两种典型的解决方法 。 第一种不太及时的方法是使用所谓的预训练模型 ,即预先在大型数据集(例如 ImageNet 数据集)上训练过的模型 。 这些经过预先训练的模型可以在不同的深度学习框架中轻松获得,并且通常被称为“模型动物园 ” 。 预训练模型的选择在很大程度上取决于当前要解决的任务是什么 ,以及数据集的大小。 选择模型后 ,我们可以使用全部或部分模型作为要解决的实际任务的初始化模型 。
深度学习的另一种不太常见的方式是自己预先训练模型 。 当可用的预训练网络不适合解决特定问题时,通常会发生这种情况 ,我们必须自己设计网络架构。 显然 ,这需要更多的时间和精力来设计模型和准备数据集 。 在某些情况下,用于进行网络预训练的数据集甚至可以是合成的 ,可以从计算机图形引擎(例如 3D Studio Max 或 Unity)或其他卷积神经网络(例如 GAN)生成 。 可以对虚拟数据进行预训练的模型在真实数据上进行微调 ,并且可以与仅对真实数据进行训练的模型一起很好地工作。
例如 ,如果我们想区分猫和狗 ,而我们没有足够的数据 ,则可以从“模型动物园”下载在 ImageNet 上训练的网络 ,并使用除最后一层以外的所有层的权重 。 最后一层必须调整为具有与类数量相同的大小 ,在本例中为两个 ,并且权重需要重新初始化和训练 。 这样 ,通过将这些层的学习率设置为零或非常小的值(请参见下图),我们将冻结那些不需训练的层 。 如果有更大的数据集 ,我们可以训练最后三个全连接层 。 有时 ,预训练网络只能用于初始化权重,然后再进行正常训练 。
迁移学习之所以有效 ,是因为在初始层计算出的特征更通用并且看起来很相似 。 在顶层提取的特征对于我们要解决的问题变得更加具体 。
为了进一步研究如何使用迁移学习 ,以及对该主题的更深刻理解,让我们看一下代码 。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hSGjbLWz-1681568428349)(https://cdn.yuucn.cn/wp-content/uploads/2023/04/1682036656-2b52160e715de12.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 层传递给):
train_step = tf.train.AdamOptimizer(learning_rate).minimize(loss, var_list=list_fc_linear)完整代码
在此示例中 ,我们将从 MNIST 卷积自编码器示例中加载权重 。 我们将仅恢复编码器部分的权重 ,冻结卷积层 ,并训练 FC 层以执行数字分类:
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 架构,以构建准确而实用的机器学习系统。
八 、机%
创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!