Python 深度学习算法实用指南(五)
原文:
zh.annas-archive.org/md5/844a6ce45a119d3197c33a6b5db2d7b1译者:飞龙
第九章:更多关于 GAN 的学习
我们学习了生成对抗网络(GANs)是什么,以及如何使用不同类型的 GAN 生成图像,参见第八章,使用 GAN 生成图像。
在本章中,我们将揭示各种有趣的不同类型的 GAN。我们已经了解到 GAN 可以用来生成新的图像,但我们无法控制它们生成的图像。例如,如果我们希望我们的 GAN 生成具有特定特征的人脸,我们如何向 GAN 传达这些信息?我们做不到,因为我们无法控制生成器生成的图像。
为了解决这个问题,我们使用一种称为Conditional GAN(CGAN)的新型 GAN,可以通过指定我们要生成的内容来条件化生成器和判别器。我们将从理解 CGAN 如何生成我们感兴趣的图像开始本章,然后学习如何使用TensorFlow实现 CGAN。
接着我们了解InfoGANs,这是 CGAN 的无监督版本。我们将了解 InfoGANs 是什么,它们与 CGAN 有何不同,以及如何使用 TensorFlow 实现它们来生成新的图像。
接下来,我们将学习关于CycleGANs的内容,这是一种非常有趣的 GAN 类型。它们试图学习从一个域中图像的分布到另一个域中图像的映射。例如,将灰度图像转换为彩色图像,我们训练 CycleGAN 学习灰度图像和彩色图像之间的映射,这意味着它们学会了从一个域映射到另一个域,最好的部分是,与其他架构不同,它们甚至不需要成对的数据集。我们将深入探讨它们如何学习这些映射以及它们的架构细节。我们将探索如何实现 CycleGAN 以将真实图片转换为绘画作品。
在本章结束时,我们将探索StackGAN,它可以将文本描述转换为逼真的图片。我们将通过深入理解其架构细节来理解 StackGAN 如何实现这一点。
在本章中,我们将学习以下内容:
-
Conditional GANs
-
使用 CGAN 生成特定数字
-
InfoGAN
-
InfoGAN 的架构
-
使用 TensorFlow 构建 InfoGAN
-
CycleGAN
-
使用 CycleGAN 将图片转换为绘画
-
StackGAN
Conditional GANs
我们知道生成器通过学习真实数据分布生成新的图像,而鉴别器则检查生成器生成的图像是来自真实数据分布还是伪数据分布。
然而,生成器有能力通过学习真实数据分布生成新颖有趣的图像。我们无法控制或影响生成器生成的图像。例如,假设我们的生成器正在生成人脸图像;我们如何告诉生成器生成具有某些特征的人脸,比如大眼睛和尖鼻子?
我们不能!因为我们无法控制生成器生成的图像。
为了克服这一点,我们引入了 GAN 的一个小变体称为 CGAN,它对生成器和鉴别器都施加了一个条件。这个条件告诉 GAN 我们希望生成器生成什么样的图像。因此,我们的两个组件——鉴别器和生成器——都会根据这个条件进行操作。
让我们考虑一个简单的例子。假设我们正在使用 MNIST 数据集和 CGAN 生成手写数字。我们假设我们更专注于生成数字 7 而不是其他数字。现在,我们需要将这个条件强加给我们的生成器和鉴别器。我们如何做到这一点?
生成器以噪声 作为输入,并生成一幅图像。但除了
外,我们还传入了额外的输入,即
。这个
是一个独热编码的类标签。由于我们希望生成数字 7,我们将第七个索引设置为 1,其余索引设置为 0,即 [0,0,0,0,0,0,0,1,0,0]。
我们将潜在向量 和独热编码的条件变量
连接起来,并将其作为输入传递给生成器。然后,生成器开始生成数字 7。
鉴别器呢?我们知道鉴别器以图像 作为输入,并告诉我们图像是真实的还是伪造的。在 CGAN 中,我们希望鉴别器基于条件进行鉴别,这意味着它必须判断生成的图像是真实的数字 7 还是伪造的数字 7。因此,除了传入输入
外,我们还通过连接
和
将条件变量
传递给鉴别器。
正如您在以下图中所看到的,我们正在传入生成器 和
:
生成器是基于 引入的信息条件。类似地,除了将真实和伪造图像传递给鉴别器外,我们还向鉴别器传递了
。因此,生成器生成数字 7,而鉴别器学会区分真实的 7 和伪造的 7。
我们刚刚学习了如何使用 CGAN 生成特定数字,但是 CGAN 的应用并不仅限于此。假设我们需要生成具有特定宽度和高度的数字。我们也可以将这个条件加到,并让 GAN 生成任何期望的图像。
CGAN 的损失函数
正如您可能已经注意到的那样,我们的普通 GAN 和 CGAN 之间没有太大区别,只是在 CGAN 中,我们将额外输入(即条件变量)与生成器和鉴别器的输入连接在一起。因此,生成器和鉴别器的损失函数与普通 GAN 相同,唯一的区别是它是有条件的
。
因此,鉴别器的损失函数如下所示:
生成器的损失函数如下所示:
使用梯度下降最小化损失函数来学习 CGAN。
使用 CGAN 生成特定手写数字
我们刚刚学习了 CGAN 的工作原理和结构。为了加强我们的理解,现在我们将学习如何在 TensorFlow 中实现 CGAN,以生成特定手写数字的图像,比如数字 7。
首先,加载所需的库:
import warnings
warnings.filterwarnings('ignore')
import numpy as np
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
tf.logging.set_verbosity(tf.logging.ERROR)
tf.reset_default_graph()
import matplotlib.pyplot as plt
%matplotlib inline
from IPython import display
加载 MNIST 数据集:
data = input_data.read_data_sets("data/mnist",one_hot=True)
定义生成器
生成器G接受噪声,,以及条件变量,
,作为输入,并返回图像。我们将生成器定义为简单的两层前馈网络:
def generator(z, c,reuse=False):
with tf.variable_scope('generator', reuse=reuse):
初始化权重:
w_init = tf.contrib.layers.xavier_initializer()
连接噪声,,和条件变量,
:
inputs = tf.concat([z, c], 1)
定义第一层:
dense1 = tf.layers.dense(inputs, 128, kernel_initializer=w_init)
relu1 = tf.nn.relu(dense1)
定义第二层,并使用tanh激活函数计算输出:
logits = tf.layers.dense(relu1, 784, kernel_initializer=w_init)
output = tf.nn.tanh(logits)
return output
定义鉴别器
我们知道鉴别器,,返回概率;也就是说,它会告诉我们给定图像为真实图像的概率。除了输入图像,
,它还将条件变量,
,作为输入。我们将鉴别器定义为简单的两层前馈网络:
def discriminator(x, c, reuse=False):
with tf.variable_scope('discriminator', reuse=reuse):
初始化权重:
w_init = tf.contrib.layers.xavier_initializer()
连接输入, 和条件变量,
:
inputs = tf.concat([x, c], 1)
定义第一层:
dense1 = tf.layers.dense(inputs, 128, kernel_initializer=w_init)
relu1 = tf.nn.relu(dense1)
定义第二层,并使用sigmoid激活函数计算输出:
logits = tf.layers.dense(relu1, 1, kernel_initializer=w_init)
output = tf.nn.sigmoid(logits)
return output
定义输入占位符,,条件变量,
,和噪声,
:
x = tf.placeholder(tf.float32, shape=(None, 784))
c = tf.placeholder(tf.float32, shape=(None, 10))
z = tf.placeholder(tf.float32, shape=(None, 100))
启动 GAN!
首先,我们将噪声, 和条件变量,
,输入到生成器中,它将输出伪造的图像,即,
:
fake_x = generator(z, c)
现在我们将真实图像一同与条件变量,
,输入到鉴别器,
,并得到它们是真实的概率:
D_logits_real = discriminator(x,c)
类似地,我们将伪造的图像,fake_x 和条件变量,,输入到鉴别器,
,并得到它们是真实的概率:
D_logits_fake = discriminator(fake_x, c, reuse=True)
计算损失函数
现在我们将看如何计算损失函数。它与普通 GAN 基本相同,只是我们添加了一个条件变量。
鉴别器损失
鉴别器损失如下所示:
首先,我们将实现第一项,即,:
D_loss_real = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=D_logits_real,
labels=tf.ones_like(D_logits_real)))
现在我们将实现第二项,:
D_loss_fake = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=D_logits_fake,
labels=tf.zeros_like(D_logits_fake)))
最终损失可以写为:
D_loss = D_loss_real + D_loss_fake
生成器损失
生成器损失如下所示:
生成器损失可以实现如下:
G_loss = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=D_logits_fake,
labels=tf.ones_like(D_logits_fake)))
优化损失
我们需要优化我们的生成器和鉴别器。因此,我们将鉴别器和生成器的参数分别收集为theta_D和theta_G:
training_vars = tf.trainable_variables()
theta_D = [var for var in training_vars if var.name.startswith('discriminator')]
theta_G = [var for var in training_vars if var.name.startswith('generator')]
使用 Adam 优化器优化损失:
learning_rate = 0.001
D_optimizer = tf.train.AdamOptimizer(learning_rate, beta1=0.5).minimize(D_loss,
var_list=theta_D)
G_optimizer = tf.train.AdamOptimizer(learning_rate, beta1=0.5).minimize(G_loss,
var_list=theta_G)
开始训练 CGAN
开始 TensorFlow 会话并初始化变量:
session = tf.InteractiveSession()
tf.global_variables_initializer().run()
定义batch_size:
batch_size = 128
定义迭代次数和类别数:
num_epochs = 500
num_classes = 10
定义图像和标签:
images = (data.train.images)
labels = data.train.labels
生成手写数字 7
我们将要生成的数字(标签)设置为7:
label_to_generate = 7
onehot = np.eye(10)
设置迭代次数:
for epoch in range(num_epochs):
for i in range(len(images) // batch_size):
基于批处理大小采样图像:
batch_image = images[i * batch_size:(i + 1) * batch_size]
随机采样条件,即我们要生成的数字:
batch_c = labels[i * batch_size:(i + 1) * batch_size]
采样噪声:
batch_noise = np.random.normal(0, 1, (batch_size, 100))
训练生成器并计算生成器损失:
generator_loss, _ = session.run([D_loss, D_optimizer], {x: batch_image, c: batch_c, z: batch_noise})
训练鉴别器并计算鉴别器损失:
discriminator_loss, _ = session.run([G_loss, G_optimizer], {x: batch_image, c: batch_c, z: batch_noise})
随机采样噪声:
noise = np.random.rand(1,100)
选择我们想要生成的数字:
gen_label = np.array([[label_to_generate]]).reshape(-1)
将选择的数字转换为一个独热编码向量:
one_hot_targets = np.eye(num_classes)[gen_label]
将噪声和独热编码条件输入到生成器中并生成伪造图像:
_fake_x = session.run(fake_x, {z: noise, c: one_hot_targets})
_fake_x = _fake_x.reshape(28,28)
打印生成器和鉴别器的损失并绘制生成器图像:
print("Epoch: {},Discriminator Loss:{}, Generator Loss: {}".format(epoch,discriminator_loss,generator_loss))
#plot the generated image
display.clear_output(wait=True)
plt.imshow(_fake_x)
plt.show()
如下图所示,生成器现在已经学会生成数字 7,而不是随机生成其他数字:
理解 InfoGAN
InfoGAN 是 CGAN 的无监督版本。在 CGAN 中,我们学习如何条件生成器和判别器以生成我们想要的图像。但是当数据集中没有标签时,我们如何做到这一点?假设我们有一个没有标签的 MNIST 数据集,我们如何告诉生成器生成我们感兴趣的特定图像?由于数据集是无标签的,我们甚至不知道数据集中存在哪些类别。
我们知道生成器使用噪声z作为输入并生成图像。生成器在z中封装了关于图像的所有必要信息,这被称为纠缠表示。它基本上学习了z中图像的语义表示。如果我们能解开这个向量,我们就可以发现图像的有趣特征。
因此,我们将把z分为两部分:
-
常规噪声
-
代码c
什么是代码?代码c基本上是可解释的分离信息。假设我们有 MNIST 数据,那么代码c1暗示了数字标签,代码c2暗示了数字的宽度,c3暗示了数字的笔画,依此类推。我们用术语c来统称它们。
现在我们有z和c,我们如何学习有意义的代码c?我们能从生成器生成的图像学习有意义的代码吗?假设生成器生成了数字 7 的图像。现在我们可以说代码c1是 7,因为我们知道c1暗示了数字标签。
但由于代码可以意味着任何东西,比如标签、数字的宽度、笔画、旋转角度等等,我们如何学习我们想要的东西?代码c将根据先验选择进行学习。例如,如果我们为c选择了一个多项先验,那么我们的 InfoGAN 可能会为c分配一个数字标签。假设我们选择了一个高斯先验,那么它可能会分配一个旋转角度等等。我们也可以有多个先验。
先验c的分布可以是任何形式。InfoGAN 根据分布分配不同的属性。在 InfoGAN 中,代码c是根据生成器输出自动推断的,不像 CGAN 那样我们需要显式指定c。
简而言之,我们基于生成器输出推断,
。但我们究竟是如何推断
的呢?我们使用信息理论中的一个概念,称为互信息。
互信息
两个随机变量之间的互信息告诉我们可以通过一个随机变量获取另一个随机变量的信息量。两个随机变量x和y之间的互信息可以表示如下:
它基本上是y的熵与给定x的条件熵之间的差异。
代码 和生成器输出
之间的互信息告诉我们通过
我们可以获取多少关于
的信息。如果互信息 c 和
很高,那么我们可以说知道生成器输出有助于推断 c。但如果互信息很低,则无法从生成器输出推断 c。我们的目标是最大化互信息。
代码 和生成器输出
之间的互信息可以表示为:
让我们来看看公式的元素:
-
是代码的熵
-
是给定生成器输出
条件下代码 c 的条件熵。
但问题是,我们如何计算 ?因为要计算这个值,我们需要知道后验分布
,而这是我们目前不知道的。因此,我们用辅助分布
来估计后验分布:
假设 ,那么我们可以推导出互信息如下:
因此,我们可以说:
最大化互信息, 基本上意味着我们在生成输出中最大化了关于 c 的知识,也就是通过另一个变量了解一个变量。
InfoGAN 的架构
好的。这里到底发生了什么?为什么我们要这样做?简单地说,我们把输入分成了两部分:z 和 c。因为 z 和 c 都用于生成图像,它们捕捉了图像的语义含义。代码 c 给出了我们关于图像的可解释解耦信息。因此,我们试图在生成器输出中找到 c。然而,我们不能轻易地做到这一点,因为我们还不知道后验分布 ,所以我们使用辅助分布
来学习 c。
这个辅助分布基本上是另一个神经网络;让我们称这个网络为 Q 网络。Q 网络的作用是预测给定生成器图像 x 的代码 c 的可能性,表示为 。
首先,我们从先验分布 p(c) 中采样 c。然后,我们将 c 和 z 连接起来,并将它们输入生成器。接下来,我们将由生成器给出的结果 输入鉴别器。我们知道鉴别器的作用是输出给定图像为真实图像的概率。此外,Q 网络接收生成的图像,并返回给定生成图像的 c 的估计。
鉴别器 D 和 Q 网络都接受生成器图像并返回输出,因此它们共享一些层。由于它们共享一些层,我们将 Q 网络附加到鉴别器上,如下图所示:
因此,鉴别器返回两个输出:
-
图像为真实图像的概率
-
c 的估计,即给定生成器图像的 c 的概率
我们将互信息项添加到我们的损失函数中。
因此,鉴别器的损失函数定义为:
生成器的损失函数定义为:
这两个方程表明我们正在最小化 GAN 的损失,并同时最大化互信息。对 InfoGAN 还有疑惑?别担心!我们将通过在 TensorFlow 中逐步实现它们来更好地学习 InfoGAN。
在 TensorFlow 中构建 InfoGAN:
我们将通过在 TensorFlow 中逐步实现 InfoGAN 来更好地理解它们。我们将使用 MNIST 数据集,并学习 InfoGAN 如何基于生成器输出自动推断出代码 。我们构建一个 Info-DCGAN;即,在生成器和鉴别器中使用卷积层而不是简单的神经网络。
首先,我们将导入所有必要的库:
import warnings
warnings.filterwarnings('ignore')
import numpy as np
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
tf.logging.set_verbosity(tf.logging.ERROR)
import matplotlib.pyplot as plt
%matplotlib inline
加载 MNIST 数据集:
data = input_data.read_data_sets("data/mnist",one_hot=True)
定义泄漏 ReLU 激活函数:
def lrelu(X, leak=0.2):
f1 = 0.5 * (1 + leak)
f2 = 0.5 * (1 - leak)
return f1 * X + f2 * tf.abs(X)
定义生成器:
生成器 ,它接受噪声
和变量
作为输入,并返回一个图像。与在生成器中使用全连接层不同,我们使用了一个反卷积网络,就像我们学习 DCGAN 时一样:
def generator(c, z,reuse=None):
首先,连接噪声 z 和变量 :
input_combined = tf.concat([c, z], axis=1)
定义第一层,这是一个包括批归一化和 ReLU 激活的全连接层:
fuly_connected1 = tf.layers.dense(input_combined, 1024)
batch_norm1 = tf.layers.batch_normalization(fuly_connected1, training=is_train)
relu1 = tf.nn.relu(batch_norm1)
定义第二层,它也是全连接层,包括批归一化和 ReLU 激活:
fully_connected2 = tf.layers.dense(relu1, 7 * 7 * 128)
batch_norm2 = tf.layers.batch_normalization(fully_connected2, training=is_train)
relu2 = tf.nn.relu(batch_norm2)
展平第二层的结果:
relu_flat = tf.reshape(relu2, [batch_size, 7, 7, 128])
第三层是反卷积,即转置卷积操作,紧随其后是批归一化和 ReLU 激活:
deconv1 = tf.layers.conv2d_transpose(relu_flat,
filters=64,
kernel_size=4,
strides=2,
padding='same',
activation=None)
batch_norm3 = tf.layers.batch_normalization(deconv1, training=is_train)
relu3 = tf.nn.relu(batch_norm3)
第四层是另一个转置卷积操作:
deconv2 = tf.layers.conv2d_transpose(relu3,
filters=1,
kernel_size=4,
strides=2,
padding='same',
activation=None)
对第四层的结果应用 sigmoid 函数并获得输出:
output = tf.nn.sigmoid(deconv2)
return output
定义鉴别器
我们学到鉴别器 和 Q 网络都接受生成器图像并返回输出,因此它们共享一些层。由于它们共享一些层,我们像在 InfoGAN 的架构中学到的那样,将 Q 网络附加到鉴别器上。在鉴别器中,我们使用卷积网络,而不是全连接层,正如我们在 DCGAN 的鉴别器中学到的:
def discriminator(x,reuse=None):
定义第一层,执行卷积操作,随后是泄漏 ReLU 激活:
conv1 = tf.layers.conv2d(x,
filters=64,
kernel_size=4,
strides=2,
padding='same',
kernel_initializer=tf.contrib.layers.xavier_initializer(),
activation=None)
lrelu1 = lrelu(conv1, 0.2)
在第二层中,我们还执行卷积操作,随后进行批归一化和泄漏 ReLU 激活:
conv2 = tf.layers.conv2d(lrelu1,
filters=128,
kernel_size=4,
strides=2,
padding='same',
kernel_initializer=tf.contrib.layers.xavier_initializer(),
activation=None)
batch_norm2 = tf.layers.batch_normalization(conv2, training=is_train)
lrelu2 = lrelu(batch_norm2, 0.2)
展平第二层的结果:
lrelu2_flat = tf.reshape(lrelu2, [batch_size, -1])
将展平的结果馈送到全连接层,这是第三层,随后进行批归一化和泄漏 ReLU 激活:
full_connected = tf.layers.dense(lrelu2_flat,
units=1024,
activation=None)
batch_norm_3 = tf.layers.batch_normalization(full_connected, training=is_train)
lrelu3 = lrelu(batch_norm_3, 0.2)
计算鉴别器输出:
d_logits = tf.layers.dense(lrelu3, units=1, activation=None)
正如我们学到的,我们将 Q 网络附加到鉴别器。定义 Q 网络的第一层,它以鉴别器的最终层作为输入:
full_connected_2 = tf.layers.dense(lrelu3,
units=128,
activation=None)
batch_norm_4 = tf.layers.batch_normalization(full_connected_2, training=is_train)
lrelu4 = lrelu(batch_norm_4, 0.2)
定义 Q 网络的第二层:
q_net_latent = tf.layers.dense(lrelu4,
units=74,
activation=None)
估计 c:
q_latents_categoricals_raw = q_net_latent[:,0:10]
c_estimates = tf.nn.softmax(q_latents_categoricals_raw, dim=1)
返回鉴别器 logits 和估计的 c 值作为输出:
return d_logits, c_estimates
定义输入占位符
现在我们为输入 、噪声
和代码
定义占位符:
batch_size = 64
input_shape = [batch_size, 28,28,1]
x = tf.placeholder(tf.float32, input_shape)
z = tf.placeholder(tf.float32, [batch_size, 64])
c = tf.placeholder(tf.float32, [batch_size, 10])
is_train = tf.placeholder(tf.bool)
启动 GAN
首先,我们将噪声 和代码
提供给生成器,它将根据方程输出假图像
:
fake_x = generator(c, z)
现在我们将真实图像 提供给鉴别器
,并得到图像为真实的概率。同时,我们还获得了对于真实图像的估计
:
D_logits_real, c_posterior_real = discriminator(x)
类似地,我们将假图像提供给鉴别器,并得到图像为真实的概率,以及对于假图像的估计 :
D_logits_fake, c_posterior_fake = discriminator(fake_x,reuse=True)
计算损失函数
现在我们将看到如何计算损失函数。
鉴别器损失
鉴别器损失如下:
作为 InfoGAN 的鉴别器损失与 CGAN 相同,实现鉴别器损失与我们在 CGAN 部分学到的相同:
#real loss
D_loss_real = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=D_logits_real,
labels=tf.ones(dtype=tf.float32, shape=[batch_size, 1])))
#fake loss
D_loss_fake = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=D_logits_fake,
labels=tf.zeros(dtype=tf.float32, shape=[batch_size, 1])))
#final discriminator loss
D_loss = D_loss_real + D_loss_fake
生成器损失
生成器的损失函数如下所示:
生成器损失的实现为:
G_loss = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=D_logits_fake,
labels=tf.ones(dtype=tf.float32, shape=[batch_size, 1])))
互信息
我们从鉴别器和生成器的损失中减去互信息。因此,鉴别器和生成器的最终损失函数如下所示:
可以计算互信息如下:
首先,我们为 定义一个先验:
c_prior = 0.10 * tf.ones(dtype=tf.float32, shape=[batch_size, 10])
当 给定时,
的熵表示为
。我们知道熵的计算如下所示:
entropy_of_c = tf.reduce_mean(-tf.reduce_sum(c * tf.log(tf.clip_by_value(c_prior, 1e-12, 1.0)),axis=-1))
当给定 时,
的条件熵为
。条件熵的代码如下:
log_q_c_given_x = tf.reduce_mean(tf.reduce_sum(c * tf.log(tf.clip_by_value(c_posterior_fake, 1e-12, 1.0)), axis=-1))
互信息表示为 :
mutual_information = entropy_of_c + log_q_c_given_x
鉴别器和生成器的最终损失如下所示:
D_loss = D_loss - mutual_information
G_loss = G_loss - mutual_information
优化损失
现在我们需要优化我们的生成器和鉴别器。因此,我们收集鉴别器和生成器的参数分别为 和
:
training_vars = tf.trainable_variables()
theta_D = [var for var in training_vars if 'discriminator' in var.name]
theta_G = [var for var in training_vars if 'generator' in var.name]
使用 Adam 优化器优化损失:
learning_rate = 0.001
D_optimizer = tf.train.AdamOptimizer(learning_rate).minimize(D_loss,var_list = theta_D)
G_optimizer = tf.train.AdamOptimizer(learning_rate).minimize(G_loss, var_list = theta_G)
开始训练
定义批量大小和轮数,并初始化所有 TensorFlow 变量:
num_epochs = 100
session = tf.InteractiveSession()
session.run(tf.global_variables_initializer())
定义一个辅助函数来可视化结果:
def plot(c, x):
c_ = np.argmax(c, 1)
sort_indices = np.argsort(c_, 0)
x_reshape = np.reshape(x[sort_indices], [batch_size, 28, 28])
x_reshape = np.reshape( np.expand_dims(x_reshape, axis=0), [4, (batch_size // 4), 28, 28])
values = []
for i in range(0,4):
row = np.concatenate( [x_reshape[i,j,:,:] for j in range(0,(batch_size // 4))], axis=1)
values.append(row)
return np.concatenate(values, axis=0)
生成手写数字
开始训练并生成图像。每100次迭代,我们打印生成器生成的图像:
onehot = np.eye(10)
for epoch in range(num_epochs):
for i in range(0, data.train.num_examples // batch_size):
样本图像:
x_batch, _ = data.train.next_batch(batch_size)
x_batch = np.reshape(x_batch, (batch_size, 28, 28, 1))
样本值 c:
c_ = np.random.randint(low=0, high=10, size=(batch_size,))
c_one_hot = onehot[c_]
样本噪声 z:
z_batch = np.random.uniform(low=-1.0, high=1.0, size=(batch_size,64))
优化生成器和鉴别器的损失:
feed_dict={x: x_batch, c: c_one_hot, z: z_batch, is_train: True}
_ = session.run(D_optimizer, feed_dict=feed_dict)
_ = session.run(G_optimizer, feed_dict=feed_dict)
每 100^(th) 次迭代打印生成器图像:
if i % 100 == 0:
discriminator_loss = D_loss.eval(feed_dict)
generator_loss = G_loss.eval(feed_dict)
_fake_x = fake_x.eval(feed_dict)
print("Epoch: {}, iteration: {}, Discriminator Loss:{}, Generator Loss: {}".format(epoch,i,discriminator_loss,generator_loss))
plt.imshow(plot(c_one_hot, _fake_x))
plt.show()
我们可以看到生成器在每次迭代中是如何发展和生成更好的数字的:
使用 CycleGAN 翻译图像
我们已经学习了几种类型的 GAN,并且它们的应用是无穷无尽的。我们已经看到生成器如何学习真实数据的分布并生成新的逼真样本。现在我们将看到一个非常不同且非常创新的 GAN 类型,称为CycleGAN。
不像其他 GAN,CycleGAN 将数据从一个域映射到另一个域,这意味着这里我们试图学习从一个域的图像分布到另一个域的图像分布的映射。简单地说,我们将图像从一个域翻译到另一个域。
这意味着什么?假设我们想要将灰度图像转换为彩色图像。灰度图像是一个域,彩色图像是另一个域。CycleGAN 学习这两个域之间的映射并在它们之间进行转换。这意味着给定一个灰度图像,CycleGAN 将其转换为彩色图像。
CycleGAN 的应用非常广泛,例如将真实照片转换为艺术图片、季节转换、照片增强等。如下图所示,您可以看到 CycleGAN 如何在不同域之间转换图像:
但 CycleGAN 有什么特别之处?它们能够在没有配对示例的情况下将图像从一个域转换到另一个域。假设我们要将照片(源)转换为绘画(目标)。在普通的图像到图像的转换中,我们如何做到这一点?我们通过收集一些照片及其对应的绘画,如下图所示,准备训练数据:
收集每个用例的这些配对数据点是一项昂贵的任务,我们可能没有太多记录或配对。这就是 CycleGAN 的最大优势所在。它不需要对齐的数据对。要将照片转换为绘画,我们只需一堆照片和一堆绘画。它们不需要进行映射或对齐。
如下图所示,我们有一些照片在一列,一些绘画在另一列;您可以看到它们并不互相配对。它们是完全不同的图像:
因此,要将图像从任何源域转换为目标域,我们只需一堆这两个域中的图像,而它们不需要成对。现在让我们看看它们是如何工作以及它们如何学习源与目标域之间的映射。
与其他生成对抗网络(GAN)不同,CycleGAN 包含两个生成器和两个判别器。我们用来代表源域中的图像,用
来代表目标域中的图像。我们需要学习
到
的映射关系。
假设我们要学习将真实图片转换为绘画
,如下图所示:
生成器的角色
我们有两个生成器,和
。
的作用是学习从
到
的映射。如上所述,
的作用是学习将照片转换为绘画,如下图所示:
它试图生成一个虚假的目标图像,这意味着它将一个源图像,,作为输入,并生成一个虚假的目标图像,
:
生成器的角色,,是学习从
到
的映射,并学会将绘画转化为真实图片,如下图所示:
它试图生成一个虚假的源图像,这意味着它将一个目标图像,,作为输入,并生成一个虚假的源图像,
:
判别器的角色
与生成器类似,我们有两个判别器,和
。判别器
的角色是区分真实源图像
和假源图像
。我们知道假源图像是由生成器
生成的。
给定一个图像给判别器,它返回图像为真实源图像的概率:
以下图显示了判别器,您可以观察到它将真实源图像 x 和生成器 F 生成的假源图像作为输入,并返回图像为真实源图像的概率:
判别器的角色是区分真实目标图像
和假目标图像
。我们知道假目标图像是由生成器
生成的。给定一个图像给判别器
,它返回图像为真实目标图像的概率:
以下图显示了判别器,您可以观察到它将真实目标图像
和生成器生成的假目标图像
作为输入,并返回图像为真实目标图像的概率:
损失函数
在 CycleGAN 中,我们有两个生成器和两个鉴别器。生成器学习将图像从一个域转换到另一个域,鉴别器试图区分转换后的图像。
因此,我们可以说鉴别器的损失函数 可以表示如下:
同样地,鉴别器 的损失函数可以表示如下:
生成器 的损失函数可以表示如下:
生成器 的损失函数可以表示如下:
总之,最终损失可以写成如下形式:
循环一致性损失
仅仅通过对抗损失并不能确保图像的正确映射。例如,生成器可以将源域中的图像映射到目标域中图像的随机排列,这可能匹配目标分布。
因此,为了避免这种情况,我们引入了一种额外的损失,称为循环一致性损失。它强制生成器 G 和 F 都是循环一致的。
让我们回顾一下生成器的功能:
-
生成器 G:将 x 转换为 y
-
生成器 F:将 y 转换为 x
我们知道生成器 G 接受源图像 x 并将其转换为虚假的目标图像 y。现在,如果我们将这个生成的虚假目标图像 y 输入生成器 F,它应该返回原始的源图像 x。有点混乱,对吧?
看看下面的图示;我们有一个源图像 x。首先,我们将这个图像输入生成器 G,它会返回一个虚假的目标图像 y。现在我们将这个虚假的目标图像 y 输入到生成器 F,它应该返回原始的源图像:
上述方程可以表示如下:
这被称为前向一致性损失,可以表示如下:
类似地,我们可以指定后向一致性损失,如下图所示。假设我们有一个原始的目标图像 y。我们将这个 y 输入鉴别器 F,它返回一个虚假的源图像 x。现在我们将这个生成的虚假源图像 x 输入生成器 G,它应该返回原始的目标图像 y:
前述方程可以表示为:
后向一致性损失可以表示如下:
因此,结合反向和正向一致性损失,我们可以将循环一致性损失写成:
我们希望我们的生成器保持循环一致,因此,我们将它们的损失与循环一致性损失相乘。因此,最终损失函数可以表示为:
使用 CycleGAN 将照片转换为绘画作品
现在我们将学习如何在 TensorFlow 中实现 CycleGAN。我们将看到如何使用 CycleGAN 将图片转换为绘画作品:
本节使用的数据集可以从 people.eecs.berkeley.edu/~taesung_park/CycleGAN/datasets/monet2photo.zip 下载。下载数据集后,请解压缩存档;它包括四个文件夹,trainA、trainB、testA 和 testB,其中包含训练和测试图像。
trainA 文件夹包含绘画作品(莫奈),而 trainB 文件夹包含照片。由于我们将照片 (x) 映射到绘画作品 (y),所以 trainB 文件夹中的照片将作为我们的源图像,,而包含绘画作品的
trainA 将作为目标图像,。
CycleGAN 的完整代码及其逐步说明可作为 Jupyter Notebook 在 github.com/PacktPublishing/Hands-On-Deep-Learning-Algorithms-with-Python 中找到。
而不是查看整段代码,我们只会看 TensorFlow 中如何实现 CycleGAN 并将源图像映射到目标领域。您也可以在 github.com/PacktPublishing/Hands-On-Deep-Learning-Algorithms-with-Python 查看完整的代码。
定义 CycleGAN 类:
class CycleGAN:
def __init__(self):
定义输入占位符 X 和输出占位符 Y:
self.X = tf.placeholder("float", shape=[batchsize, image_height, image_width, 3])
self.Y = tf.placeholder("float", shape=[batchsize, image_height, image_width, 3])
定义生成器,,将
映射到
:
G = generator("G")
定义生成器,,将
映射到
:
F = generator("F")
定义辨别器,,用于区分真实源图像和虚假源图像:
self.Dx = discriminator("Dx")
定义辨别器,,用于区分真实目标图像和虚假目标图像:
self.Dy = discriminator("Dy")
生成虚假源图像:
self.fake_X = F(self.Y)
生成虚假目标图像:
self.fake_Y = G(self.X)
获取 logits:
#real source image logits
self.Dx_logits_real = self.Dx(self.X)
#fake source image logits
self.Dx_logits_fake = self.Dx(self.fake_X, True)
#real target image logits
self.Dy_logits_fake = self.Dy(self.fake_Y, True)
#fake target image logits
self.Dy_logits_real = self.Dy(self.Y)
我们知道循环一致性损失如下所示:
我们可以如下实现循环一致性损失:
self.cycle_loss = tf.reduce_mean(tf.abs(F(self.fake_Y, True) - self.X)) + \
tf.reduce_mean(tf.abs(G(self.fake_X, True) - self.Y))
定义我们两个鉴别器的损失, 和
。
我们可以如下重写带有 Wasserstein 距离的鉴别器损失函数:
因此,两个鉴别器的损失如下实现:
self.Dx_loss = -tf.reduce_mean(self.Dx_logits_real) + tf.reduce_mean(self.Dx_logits_fake)
self.Dy_loss = -tf.reduce_mean(self.Dy_logits_real) + tf.reduce_mean(self.Dy_logits_fake)
定义我们两个生成器的损失, 和
。我们可以如下重写带有 Wasserstein 距离的生成器损失函数:
因此,两个生成器的损失乘以循环一致性损失cycle_loss如下实现:
self.G_loss = -tf.reduce_mean(self.Dy_logits_fake) + 10\. * self.cycle_loss
self.F_loss = -tf.reduce_mean(self.Dx_logits_fake) + 10\. * self.cycle_loss
使用 Adam 优化器优化鉴别器和生成器:
#optimize the discriminator
self.Dx_optimizer = tf.train.AdamOptimizer(2e-4, beta1=0., beta2=0.9).minimize(self.Dx_loss, var_list=[self.Dx.var])
self.Dy_optimizer = tf.train.AdamOptimizer(2e-4, beta1=0., beta2=0.9).minimize(self.Dy_loss, var_list=[self.Dy.var])
#optimize the generator
self.G_optimizer = tf.train.AdamOptimizer(2e-4, beta1=0., beta2=0.9).minimize(self.G_loss, var_list=[G.var])
self.F_optimizer = tf.train.AdamOptimizer(2e-4, beta1=0., beta2=0.9).minimize(self.F_loss, var_list=[F.var])
一旦我们开始训练模型,我们可以看到鉴别器和生成器的损失随迭代次数而减小:
Epoch: 0, iteration: 0, Dx Loss: -0.6229429245, Dy Loss: -2.42867970467, G Loss: 1385.33557129, F Loss: 1383.81530762, Cycle Loss: 138.448059082
Epoch: 0, iteration: 50, Dx Loss: -6.46077537537, Dy Loss: -7.29514217377, G Loss: 629.768066406, F Loss: 615.080932617, Cycle Loss: 62.6807098389
Epoch: 1, iteration: 100, Dx Loss: -16.5891685486, Dy Loss: -16.0576553345, G Loss: 645.53137207, F Loss: 649.854919434, Cycle Loss: 63.9096908569
StackGAN
现在我们将看到一种最引人入胜和迷人的 GAN 类型,称为StackGAN。你能相信如果我说 StackGANs 可以根据文本描述生成逼真的图像吗?是的,他们可以。给定一个文本描述,他们可以生成一个逼真的图像。
让我们首先了解艺术家如何画出一幅图像。在第一阶段,艺术家绘制原始形状,创建图像的基本轮廓,形成图像的初始版本。在接下来的阶段,他们通过使图像更真实和吸引人来增强图像。
StackGANs 以类似的方式工作。它们将生成图像的过程分为两个阶段。就像艺术家画图一样,在第一阶段,他们生成基础轮廓、原始形状,并创建图像的低分辨率版本;在第二阶段,他们通过使图像更真实和吸引人来增强第一阶段生成的图像,并将其转换为高分辨率图像。
但是 StackGANs 是如何做到的呢?
他们使用两个 GAN,每个阶段一个。第一阶段的 GAN 生成基础图像,并将其发送到下一阶段的 GAN,后者将基础低分辨率图像转换为合适的高分辨率图像。下图显示了 StackGANs 如何基于文本描述在每个阶段生成图像:
正如你所见,在第一阶段,我们有图像的低分辨率版本,但在第二阶段,我们有清晰的高分辨率图像。但是,StackGAN 是如何做到的呢?记住,当我们用条件 GAN 学习时,我们可以通过条件化使我们的 GAN 生成我们想要的图像。
我们在两个阶段都使用它们。在第一阶段,我们的网络基于文本描述进行条件设定。根据这些文本描述,它们生成图像的基本版本。在第二阶段,我们的网络基于从第一阶段生成的图像和文本描述进行条件设定。
但为什么我们在第二阶段又必须要基于文本描述进行条件设定呢?因为在第一阶段,我们错过了文本描述中指定的一些细节,只创建了图像的基本版本。所以,在第二阶段,我们再次基于文本描述进行条件设定,修复缺失的信息,并且使我们的图像更加真实。
凭借这种仅基于文本生成图片的能力,它被广泛应用于许多应用场景。尤其在娱乐行业中大量使用,例如根据描述创建帧,还可以用于生成漫画等等。
StackGAN 的架构
现在我们对 StackGAN 的工作原理有了基本的了解,我们将更详细地查看它们的架构,看看它们如何从文本生成图片。
StackGAN 的完整架构如下图所示:
我们将逐个查看每个组件。
条件增强
我们将文本描述作为 GAN 的输入。基于这些描述,它必须生成图像。但它们如何理解文本的含义以生成图片呢?
首先,我们使用编码器将文本转换为嵌入。我们用 表示这个文本嵌入。我们能够创建
的变化吗?通过创建文本嵌入的变化,我们可以获得额外的训练对,并增加对小扰动的鲁棒性。
让 表示均值,
表示我们文本嵌入的对角协方差矩阵,
。现在我们从独立的高斯分布中随机采样一个额外的条件变量,
,帮助我们创建具有其含义的文本描述的变化。我们知道同样的文本可以用多种方式书写,因此通过条件变量
,我们可以得到映射到图像的文本的各种版本。
因此,一旦我们有了文本描述,我们将使用编码器提取它们的嵌入,然后计算它们的均值和协方差。然后,我们从文本嵌入的高斯分布中采样 。
第一阶段
现在,我们有一个文本嵌入,,以及一个条件变量,
。我们将看看它是如何被用来生成图像的基本版本。
生成器
生成器的目标是通过学习真实数据分布来生成虚假图像。首先,我们从高斯分布中采样噪声并创建z。然后,我们将z与我们的条件变量,,连接起来,并将其作为输入提供给生成器,生成图像的基本版本。
生成器的损失函数如下:
让我们来看一下这个公式:
-
表示我们从虚假数据分布中采样z,也就是先验噪声。
-
表示我们从真实数据分布中采样文本描述,
。
-
表示生成器接受噪声和条件变量返回图像。我们将这个生成的图像馈送给鉴别器。
-
表示生成的图像为虚假的对数概率。
除了这个损失之外,我们还向损失函数中添加了一个正则化项,,这意味着标准高斯分布和条件高斯分布之间的 KL 散度。这有助于我们避免过拟合。
因此,生成器的最终损失函数如下:
鉴别器
现在我们将这个生成的图像馈送给鉴别器,它返回图像为真实的概率。鉴别器的损失如下:
在这里:
-
表示真实图像,
,条件于文本描述,
-
表示生成的虚假图像
第二阶段
我们已经学习了第一阶段如何生成图像的基本版本。现在,在第二阶段,我们修复了第一阶段生成的图像的缺陷,并生成了更真实的图像版本。我们用来条件化网络的图像来自前一个阶段生成的图像,还有文本嵌入。
生成器
在第二阶段,生成器不再接受噪声作为输入,而是接受从前一阶段生成的图像作为输入,并且以文本描述作为条件。
这里, 意味着我们从阶段 I 生成的图像进行采样。
意味着我们从给定的真实数据分布
中抽样文本。
随后,生成器的损失可以如下给出:
除了正则化器外,生成器的损失函数为:
鉴别器
判别器的目标是告诉我们图像来自真实分布还是生成器分布。因此,判别器的损失函数如下:
摘要
我们从学习条件 GAN 开始这一章,了解了它们如何用于生成我们感兴趣的图像。
后来,我们学习了 InfoGAN,其中代码 c 是根据生成的输出自动推断的,不像 CGAN 那样我们需要显式指定 c。要推断 c,我们需要找到后验分布 ,而我们无法访问它。因此,我们使用辅助分布。我们使用互信息来最大化
,以增强对 c 在给定生成器输出的知识。
接着,我们学习了 CycleGAN,它将数据从一个域映射到另一个域。我们尝试学习将照片域图像分布映射到绘画域图像分布的映射关系。最后,我们了解了 StackGANs 如何从文本描述生成逼真的图像。
在下一章中,我们将学习自编码器及其类型。
问题
回答以下问题,以评估你从本章学到了多少内容:
-
条件 GAN 和普通 GAN 有何不同?
-
InfoGAN 中的代码称为什么?
-
什么是互信息?
-
为什么 InfoGAN 需要辅助分布?
-
什么是循环一致性损失?
-
解释 CycleGAN 中生成器的作用。
-
StackGANs 如何将文本描述转换为图片?
进一步阅读
更多信息,请参考以下链接:
-
条件生成对抗网络,作者为 Mehdi Mirza 和 Simon Osindero,
arxiv.org/pdf/1411.1784.pdf -
InfoGAN: 通过最大化信息的生成对抗网络进行可解释表示学习,作者为陈希等人,
arxiv.org/pdf/1606.03657.pdf -
利用循环一致性对抗网络进行非配对图像到图像翻译,作者为朱俊彦等人,
arxiv.org/pdf/1703.10593.pdf
第十章:使用自编码器重建输入
自编码器是一种无监督学习算法。与其他算法不同,自编码器学习重建输入,即自编码器接收输入并学习将其重现为输出。我们从理解什么是自编码器及其如何重建输入开始这一章节。然后,我们将学习自编码器如何重建 MNIST 图像。
接下来,我们将了解自编码器的不同变体;首先,我们将学习使用卷积层的卷积自编码器(CAEs);然后,我们将学习去噪自编码器(DAEs),它们学习如何去除输入中的噪音。之后,我们将了解稀疏自编码器及其如何从稀疏输入中学习。在本章末尾,我们将学习一种有趣的生成型自编码器,称为变分自编码器。我们将理解变分自编码器如何学习生成新的输入及其与其他自编码器的不同之处。
在本章中,我们将涵盖以下主题:
-
自编码器及其架构
-
使用自编码器重建 MNIST 图像
-
卷积自编码器
-
构建卷积自编码器
-
降噪自编码器
-
使用降噪自编码器去除图像中的噪音
-
稀疏自编码器
-
紧束缚自编码器
-
变分自编码器
什么是自编码器?
自编码器是一种有趣的无监督学习算法。与其他神经网络不同,自编码器的目标是重建给定的输入;即自编码器的输出与输入相同。它由称为编码器和解码器的两个重要组件组成。
编码器的作用是通过学习输入的潜在表示来编码输入,解码器的作用是从编码器生成的潜在表示中重建输入。潜在表示也称为瓶颈或编码。如下图所示,将图像作为输入传递给自编码器。编码器获取图像并学习图像的潜在表示。解码器获取潜在表示并尝试重建图像:
下图显示了一个简单的双层香草自编码器;正如您可能注意到的,它由输入层、隐藏层和输出层组成。首先,我们将输入馈送到输入层,然后编码器学习输入的表示并将其映射到瓶颈。从瓶颈处,解码器重建输入:
我们可能会想知道这的用途是什么。为什么我们需要编码和解码输入?为什么我们只需重建输入?嗯,有各种应用,如降维、数据压缩、图像去噪等。
由于自编码器重构输入,输入层和输出层中的节点数始终相同。假设我们有一个包含 100 个输入特征的数据集,我们有一个神经网络,其输入层有 100 个单元,隐藏层有 50 个单元,输出层有 100 个单元。当我们将数据集输入到自编码器中时,编码器尝试学习数据集中的重要特征,并将特征数减少到 50 并形成瓶颈。瓶颈保存数据的表示,即数据的嵌入,并仅包含必要信息。然后,将瓶颈输入到解码器中以重构原始输入。如果解码器成功地重构了原始输入,那么说明编码器成功地学习了给定输入的编码或表示。也就是说,编码器成功地将包含 100 个特征的数据集编码成仅包含 50 个特征的表示,通过捕获必要信息。
因此,编码器本质上尝试学习如何在不丢失有用信息的情况下减少数据的维度。我们可以将自编码器视为类似于主成分分析(PCA)的降维技术。在 PCA 中,我们通过线性变换将数据投影到低维,并去除不需要的特征。PCA 和自编码器的区别在于 PCA 使用线性变换进行降维,而自编码器使用非线性变换。
除了降维之外,自编码器还广泛用于去噪图像、音频等中的噪声。我们知道自编码器中的编码器通过仅学习必要信息并形成瓶颈或代码来减少数据集的维度。因此,当噪声图像作为自编码器的输入时,编码器仅学习图像的必要信息并形成瓶颈。由于编码器仅学习表示图像的重要和必要信息,它学习到噪声是不需要的信息并从瓶颈中移除噪声的表示。
因此,现在我们将会有一个瓶颈,也就是说,一个没有任何噪声信息的图像的表示。当编码器学习到这个被称为瓶颈的编码表示时,将其输入到解码器中,解码器会从编码器生成的编码中重建输入图像。由于编码没有噪声,重建的图像将不包含任何噪声。
简而言之,自编码器将我们的高维数据映射到一个低级表示。这种数据的低级表示被称为潜在表示或瓶颈,只包含代表输入的有意义和重要特征。
由于我们的自编码器的角色是重建其输入,我们使用重构误差作为我们的损失函数,这意味着我们试图了解解码器正确重构输入的程度。因此,我们可以使用均方误差损失作为我们的损失函数来量化自编码器的性能。
现在我们已经了解了什么是自编码器,我们将在下一节探讨自编码器的架构。
理解自编码器的架构
正如我们刚刚学到的,自编码器由两个重要组件组成:编码器 和解码器
。让我们仔细看看它们各自的作用:
- 编码器:编码器
学习输入并返回输入的潜在表示。假设我们有一个输入,
。当我们将输入馈送给编码器时,它返回输入的低维潜在表示,称为编码或瓶颈,
。我们用
表示编码器的参数:
- 解码器:解码器
尝试使用编码器的输出,即编码
作为输入来重建原始输入
。重建图像由
表示。我们用
表示解码器的参数:
我们需要学习编码器和解码器的优化参数,分别表示为 和
,以便我们可以最小化重构损失。我们可以定义我们的损失函数为实际输入与重构输入之间的均方误差:
这里, 是训练样本的数量。
当潜在表示的维度小于输入时,这被称为欠完备自编码器。由于维度较小,欠完备自编码器试图学习和保留输入的仅有的有用和重要特征,并去除其余部分。当潜在表示的维度大于或等于输入时,自编码器将只是复制输入而不学习任何有用的特征,这种类型的自编码器称为过完备自编码器。
下图显示了欠完备和过完备自编码器。欠完备自编码器在隐藏层(code)中的神经元少于输入层中的单元数;而在过完备自编码器中,隐藏层(code)中的神经元数大于输入层中的单元数:
因此,通过限制隐藏层(code)中的神经元,我们可以学习输入的有用表示。自编码器也可以有任意数量的隐藏层。具有多个隐藏层的自编码器称为多层自编码器或深层自编码器。到目前为止,我们所学到的只是普通或浅层自编码器。
使用自编码器重构 MNIST 图像
现在我们将学习如何使用 MNIST 数据集重构手写数字的自编码器。首先,让我们导入必要的库:
import warnings
warnings.filterwarnings('ignore')
import numpy as np
import tensorflow as tf
from tensorflow.keras.models import Model
from tensorflow.keras.layers import Input, Dense
tf.logging.set_verbosity(tf.logging.ERROR)
#plotting
import matplotlib.pyplot as plt
%matplotlib inline
#dataset
from tensorflow.keras.datasets import mnist
准备数据集
让我们加载 MNIST 数据集。由于我们正在重建给定的输入,所以不需要标签。因此,我们只加载 x_train 用于训练和 x_test 用于测试:
(x_train, _), (x_test, _) = mnist.load_data()
通过除以最大像素值 255 来对数据进行归一化:
x_train = x_train.astype('float32') / 255
x_test = x_test.astype('float32') / 255
打印我们数据集的 shape:
print(x_train.shape, x_test.shape)
((60000, 28, 28), (10000, 28, 28))
将图像重塑为二维数组:
x_train = x_train.reshape((len(x_train), np.prod(x_train.shape[1:])))
x_test = x_test.reshape((len(x_test), np.prod(x_test.shape[1:])))
现在,数据的形状将变为如下所示:
print(x_train.shape, x_test.shape)
((60000, 784), (10000, 784))
定义编码器
现在我们定义编码器层,它将图像作为输入并返回编码。
定义编码的大小:
encoding_dim = 32
定义输入的占位符:
input_image = Input(shape=(784,))
定义编码器,它接受 input_image 并返回编码:
encoder = Dense(encoding_dim, activation='relu')(input_image)
定义解码器
让我们定义解码器,它从编码器中获取编码值并返回重构的图像:
decoder = Dense(784, activation='sigmoid')(encoder)
构建模型
现在我们定义了编码器和解码器,我们定义一个模型,该模型接受图像作为输入,并返回解码器的输出,即重构图像:
model = Model(inputs=input_image, outputs=decoder)
让我们看看模型的摘要:
model.summary()
________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input_1 (InputLayer) (None, 784) 0
_________________________________________________________________
dense (Dense) (None, 32) 25120
_________________________________________________________________
dense_1 (Dense) (None, 784) 25872
=================================================================
Total params: 50,992
Trainable params: 50,992
Non-trainable params: 0
_________________________________________________________________
使用二元交叉熵作为损失编译模型,并使用 adadelta 优化器最小化损失:
model.compile(optimizer='adadelta', loss='binary_crossentropy')
现在让我们训练模型。
通常,我们按 model.fit(x,y) 训练模型,其中 x 是输入,y 是标签。但由于自编码器重构它们的输入,模型的输入和输出应该相同。因此,在这里,我们按 model.fit(x_train, x_train) 训练模型:
model.fit(x_train, x_train, epochs=50, batch_size=256, shuffle=True, validation_data=(x_test, x_test))
重构图像
现在我们已经训练了模型,我们看看模型如何重构测试集的图像。将测试图像输入模型并获取重构的图像:
reconstructed_images = model.predict(x_test)
绘制重构图像
首先,让我们绘制实际图像,即输入图像:
n = 7
plt.figure(figsize=(20, 4))
for i in range(n):
ax = plt.subplot(1, n, i+1)
plt.imshow(x_test[i].reshape(28, 28))
plt.gray()
ax.get_xaxis().set_visible(False)
ax.get_yaxis().set_visible(False)
plt.show()
实际图像的绘制如下所示:
绘制重构图像如下所示:
n = 7
plt.figure(figsize=(20, 4))
for i in range(n):
ax = plt.subplot(2, n, i + n + 1)
plt.imshow(reconstructed_images[i].reshape(28, 28))
plt.gray()
ax.get_xaxis().set_visible(False)
ax.get_yaxis().set_visible(False)
plt.show()
以下显示了重构后的图像:
如您所见,自编码器已经学习了输入图像的更好表示并对其进行了重构。
使用卷积的自编码器
我们刚刚在前一节学习了什么是自编码器。我们了解了传统自编码器,它基本上是具有一个隐藏层的前馈浅网络。我们可以不将它们保持为前馈网络,而是可以将它们作为卷积网络使用吗?由于我们知道卷积网络在分类和识别图像方面表现良好(前提是在自编码器中使用卷积层而不是前馈层),当输入是图像时,它将学习更好地重建输入。
因此,我们介绍一种新类型的自编码器称为 CAE,它使用卷积网络而不是传统的神经网络。在传统的自编码器中,编码器和解码器基本上是一个前馈网络。但在 CAE 中,它们基本上是卷积网络。这意味着编码器由卷积层组成,解码器由转置卷积层组成,而不是前馈网络。CAE 如下图所示:
如图所示,我们将输入图像提供给编码器,编码器由卷积层组成,卷积层执行卷积操作并从图像中提取重要特征。然后我们执行最大池化以仅保留图像的重要特征。以类似的方式,我们执行多个卷积和最大池化操作,并获得图像的潜在表示,称为瓶颈。
接下来,我们将瓶颈输入解码器,解码器由反卷积层组成,反卷积层执行反卷积操作并试图从瓶颈中重建图像。它包括多个反卷积和上采样操作以重建原始图像。
因此,这就是 CAE 如何在编码器中使用卷积层和在解码器中使用转置卷积层来重建图像的方法。
构建卷积自编码器
就像我们在前一节学习如何实现自编码器一样,实现 CAE 也是一样的,唯一的区别是这里我们在编码器和解码器中使用卷积层,而不是前馈网络。我们将使用相同的 MNIST 数据集来使用 CAE 重建图像。
导入库:
import warnings
warnings.filterwarnings('ignore')
#modelling
from tensorflow.keras.models import Model
from tensorflow.keras.layers import Input, Dense, Conv2D, MaxPooling2D, UpSampling2D
from tensorflow.keras import backend as K
#plotting
import matplotlib.pyplot as plt
%matplotlib inline
#dataset
from keras.datasets import mnist
import numpy as np
读取并重塑数据集:
(x_train, _), (x_test, _) = mnist.load_data()
# Normalize the dataset
x_train = x_train.astype('float32') / 255.
x_test = x_test.astype('float32') / 255.
# reshape
x_train = np.reshape(x_train, (len(x_train), 28, 28, 1))
x_test = np.reshape(x_test, (len(x_test), 28, 28, 1))
让我们定义我们输入图像的形状:
input_image = Input(shape=(28, 28, 1))
定义编码器
现在,让我们定义我们的编码器。与传统自编码器不同,在这里我们使用卷积网络而不是前馈网络。因此,我们的编码器包括三个卷积层,后跟具有relu激活函数的最大池化层。
定义第一个卷积层,然后进行最大池化操作:
x = Conv2D(16, (3, 3), activation='relu', padding='same')(input_image)
x = MaxPooling2D((2, 2), padding='same')(x)
定义第二个卷积和最大池化层:
x = Conv2D(8, (3, 3), activation='relu', padding='same')(x)
x = MaxPooling2D((2, 2), padding='same')(x)
定义最终的卷积和最大池化层:
x = Conv2D(8, (3, 3), activation='relu', padding='same')(x)
encoder = MaxPooling2D((2, 2), padding='same')(x)
定义解码器
现在,我们定义我们的解码器;在解码器中,我们执行三层反卷积操作,即对编码器创建的编码进行上采样并重建原始图像。
定义第一个卷积层,并进行上采样:
x = Conv2D(8, (3, 3), activation='relu', padding='same')(encoder)
x = UpSampling2D((2, 2))(x)
定义第二个卷积层,并进行上采样:
x = Conv2D(8, (3, 3), activation='relu', padding='same')(x)
x = UpSampling2D((2, 2))(x)
定义最终的卷积层并进行上采样:
x = Conv2D(16, (3, 3), activation='relu')(x)
x = UpSampling2D((2, 2))(x)
decoded = Conv2D(1, (3, 3), activation='sigmoid', padding='same')(x)
构建模型
定义接收输入图像并返回解码器生成的图像(即重建图像)的模型:
model = Model(input_image, decoder)
让我们使用二进制交叉熵作为损失来编译模型,并使用adadelta作为优化器:
model.compile(optimizer='adadelta', loss='binary_crossentropy')
接下来,按以下方式训练模型:
model.fit(x_train, x_train, epochs=50,batch_size=128, shuffle=True, validation_data=(x_test, x_test))
重建图像
使用我们训练好的模型重建图像:
reconstructed_images = model.predict(x_test)
首先,让我们绘制输入图像:
n = 7
plt.figure(figsize=(20, 4))
for i in range(n):
ax = plt.subplot(1, n, i+1)
plt.imshow(x_test[i].reshape(28, 28))
plt.gray()
ax.get_xaxis().set_visible(False)
ax.get_yaxis().set_visible(False)
plt.show()
输入图像的绘图如下所示:
现在,我们绘制重建的图像:
n = 7
plt.figure(figsize=(20, 4))
for i in range(n):
ax = plt.subplot(2, n, i + n + 1)
plt.imshow(reconstructed_images[i].reshape(28, 28))
plt.gray()
ax.get_xaxis().set_visible(False)
ax.get_yaxis().set_visible(False)
plt.show()
重建图像的绘图如下所示:
探索去噪自编码器
去噪自编码器(DAE)是自编码器的另一种小变体。它们主要用于去除图像、音频和其他输入中的噪音。因此,当我们将破坏的输入提供给 DAE 时,它学会重建原始未破坏的输入。现在我们来看看 DAE 如何去除噪音。
使用 DAE 时,我们不是直接将原始输入馈送给自编码器,而是通过添加一些随机噪音来破坏输入,然后再馈送破坏的输入。我们知道编码器通过仅保留重要信息来学习输入的表示,并将压缩表示映射到瓶颈。当破坏的输入被送到编码器时,编码器将学习到噪音是不需要的信息,并且移除其表示。因此,编码器通过仅保留必要信息来学习无噪音的输入的紧凑表示,并将学习到的表示映射到瓶颈。
现在解码器尝试使用由编码器学习到的表示重建图像,也就是瓶颈。由于该表示不包含任何噪音,解码器在没有噪音的情况下重建输入。这就是去噪自编码器从输入中去除噪音的方式。
典型的 DAE 如下图所示。首先,我们通过添加一些噪音来破坏输入,然后将破坏的输入提供给编码器,编码器学习到去除噪音的输入的表示,而解码器使用编码器学习到的表示重建未破坏的输入:
数学上,这可以表示如下。
假设我们有一张图片,,我们向图片添加噪声后得到
,这是被破坏的图片:
现在将此破坏的图像馈送给编码器:
解码器尝试重建实际图像:
使用 DAE 进行图像去噪
在本节中,我们将学习如何使用 DAE 对图像进行去噪。我们使用 CAE 来对图像进行去噪。DAE 的代码与 CAE 完全相同,只是这里我们在输入中使用了嘈杂的图像。而不是查看整个代码,我们只会看到相应的更改。完整的代码可以在 GitHub 上查看:github.com/PacktPublishing/Hands-On-Deep-Learning-Algorithms-with-Python。
设置噪声因子:
noise_factor = 1
向训练和测试图像添加噪声:
x_train_noisy = x_train + noise_factor * np.random.normal(loc=0.0, scale=1.0, size=x_train.shape)
x_test_noisy = x_test + noise_factor * np.random.normal(loc=0.0, scale=1.0, size=x_test.shape)
将训练集和测试集裁剪为 0 和 1:
x_train_noisy = np.clip(x_train_noisy, 0., 1.)
x_test_noisy = np.clip(x_test_noisy, 0., 1.)
让我们训练模型。由于我们希望模型学会去除图像中的噪声,因此模型的输入是嘈杂的图像,即 x_train_noisy,输出是去噪后的图像,即 x_train:
model.fit(x_train_noisy, x_train, epochs=50,batch_size=128, shuffle=True, validation_data=(x_test_noisy, x_test))
使用我们训练好的模型重建图像:
reconstructed_images = model.predict(x_test_noisy)
首先,让我们绘制输入图像,即损坏的图像:
n = 7
plt.figure(figsize=(20, 4))
for i in range(n):
ax = plt.subplot(1, n, i+1)
plt.imshow(x_test_noisy[i].reshape(28, 28))
plt.gray()
ax.get_xaxis().set_visible(False)
ax.get_yaxis().set_visible(False)
plt.show()
下图显示了输入嘈杂图像的绘图:
现在,让我们绘制模型重建的图像:
n = 7
plt.figure(figsize=(20, 4))
for i in range(n):
ax = plt.subplot(2, n, i + n + 1)
plt.imshow(reconstructed_images[i].reshape(28, 28))
plt.gray()
ax.get_xaxis().set_visible(False)
ax.get_yaxis().set_visible(False)
plt.show()
如您所见,我们的模型已经学会从图像中去除噪声:
理解稀疏自编码器
我们知道自编码器学习重建输入。但是当我们设置隐藏层中的节点数大于输入层中的节点数时,它将学习一个恒等函数,这是不利的,因为它只是完全复制输入。
在隐藏层中增加更多节点有助于学习稳健的潜在表示。但是当隐藏层中的节点更多时,自编码器会试图完全模仿输入,从而过度拟合训练数据。为了解决过拟合问题,我们在损失函数中引入了一个称为稀疏约束或稀疏惩罚的新约束。带有稀疏惩罚的损失函数可以表示如下:
第一项 表示原始输入
与重建输入
之间的重构误差。第二项表示稀疏约束。现在我们将探讨这种稀疏约束如何缓解过拟合问题。
通过稀疏约束,我们仅激活隐藏层中特定的神经元,而不是激活所有神经元。根据输入,我们激活和取消激活特定的神经元,因此当这些神经元被激活时,它们将学会从输入中提取重要特征。通过施加稀疏惩罚,自编码器不会精确复制输入到输出,并且它还可以学习到稳健的潜在表示。
如下图所示,稀疏自编码器的隐藏层单元数比输入层多;然而,只有少数隐藏层中的神经元被激活。未阴影的神经元表示当前激活的神经元:
如果神经元活跃则返回 1,非活跃则返回 0。在稀疏自编码器中,我们将大多数隐藏层中的神经元设置为非活跃状态。我们知道 sigmoid 激活函数将值压缩到 0 到 1 之间。因此,当我们使用 sigmoid 激活函数时,我们尝试将神经元的值保持接近于 0。
我们通常试图保持隐藏层中每个神经元的平均激活值接近于零,比如 0.05,但不等于零,这个值被称为 ,即我们的稀疏参数。我们通常将
的值设为 0.05。
首先,我们计算神经元的平均激活值。
在整个训练集上,隐藏层中 神经元的平均激活可以计算如下:
在这里,以下内容成立:
-
表示隐藏层中
神经元的平均激活
-
是训练样本的编号
-
是隐藏层中
神经元的激活
-
是稀疏自编码器的训练样本
-
表示隐藏层中 ![] 神经元对于第 ![] 个训练样本的激活
我们努力使神经元的平均激活值 接近于
。也就是说,我们尝试保持神经元的平均激活值接近于 0.05:
因此,我们对值 进行惩罚,其变化范围为
。我们知道Kullback-Leibler(KL)散度广泛用于衡量两个概率分布之间的差异。因此,在这里,我们使用 KL 散度来衡量两个伯努利分布,即平均
和平均
,可以表示如下:
在之前的方程中,表示隐藏层
,
表示隐藏层
中的神经元。前述方程基本上是稀疏惩罚或稀疏性约束。因此,通过稀疏约束,所有神经元永远不会同时活动,并且平均而言,它们被设置为 0.05。
现在我们可以根据稀疏惩罚重新编写损失函数,如下所示:
因此,稀疏自编码器允许我们在隐藏层中拥有比输入层更多的节点,然而通过损失函数中的稀疏约束来减少过拟合问题。
构建稀疏自编码器
构建稀疏自编码器与构建常规自编码器相同,只是在编码器和解码器中使用稀疏正则化器,因此在下面的部分中我们只会看到与实现稀疏正则化器相关的部分;完整的代码及解释可以在 GitHub 上找到。
定义稀疏正则化器
下面是定义稀疏正则化器的代码:
def sparse_regularizer(activation_matrix):
将我们的 值设为
0.05:
rho = 0.05
计算 ,即平均激活值:
rho_hat = K.mean(activation_matrix)
根据方程*(1)*计算平均 和平均
之间的 KL 散度:
KL_divergence = K.sum(rho*(K.log(rho/rho_hat)) + (1-rho)*(K.log(1-rho/1-rho_hat)))
求和 KL 散度值:
sum = K.sum(KL_divergence)
将sum乘以beta并返回结果:
return beta * sum
稀疏正则化器的整个函数定义如下:
def sparse_regularizer(activation_matrix):
p = 0.01
beta = 3
p_hat = K.mean(activation_matrix)
KL_divergence = p*(K.log(p/p_hat)) + (1-p)*(K.log(1-p/1-p_hat))
sum = K.sum(KL_divergence)
return beta * sum
学习使用收缩自编码器
类似于稀疏自编码器,收缩自编码器在自编码器的损失函数中添加了新的正则化项。它们试图使我们的编码对训练数据中的小变化不那么敏感。因此,使用收缩自编码器,我们的编码变得更加稳健,对于训练数据中存在的噪声等小扰动更加鲁棒。我们现在引入一个称为正则化器或惩罚项的新术语到我们的损失函数中。它有助于惩罚对输入过于敏感的表示。
我们的损失函数可以用数学方式表示如下:
第一项表示重构误差,第二项表示惩罚项或正则化器,基本上是雅可比矩阵的Frobenius 范数。等等!这是什么意思?
矩阵的 Frobenius 范数,也称为Hilbert-Schmidt 范数,定义为其元素的绝对值平方和的平方根。由向量值函数的偏导数组成的矩阵称为雅可比矩阵。
因此,计算雅可比矩阵的 Frobenius 范数意味着我们的惩罚项是隐藏层对输入的所有偏导数的平方和。其表示如下:
计算隐藏层对输入的偏导数类似于计算损失的梯度。假设我们使用 sigmoid 激活函数,则隐藏层对输入的偏导数表示如下:
将惩罚项添加到我们的损失函数中有助于减少模型对输入变化的敏感性,并使我们的模型更加鲁棒,能够抵抗异常值。因此,收缩自编码器减少了模型对训练数据中小变化的敏感性。
实现收缩自编码器
建立收缩自编码器与建立普通自编码器几乎相同,只是在模型中使用了收缩损失正则化器,因此我们将只查看与实现收缩损失相关的部分,而不是整个代码。
定义收缩损失
现在让我们看看如何在 Python 中定义损失函数。
定义均方损失如下:
MSE = K.mean(K.square(actual - predicted), axis=1)
从我们的编码器层获取权重并转置权重:
weights = K.variable(value=model.get_layer('encoder_layer').get_weights()[0])
weights = K.transpose(weights)
获取我们的编码器层的输出:
h = model.get_layer('encoder_layer').output
定义惩罚项:
penalty_term = K.sum(((h * (1 - h))**2) * K.sum(weights**2, axis=1), axis=1)
最终损失是均方误差和乘以lambda的惩罚项的总和:
Loss = MSE + (lambda * penalty_term)
收缩损失的完整代码如下所示:
def contractive_loss(y_pred, y_true):
lamda = 1e-4
MSE = K.mean(K.square(y_true - y_pred), axis=1)
weights = K.variable(value=model.get_layer('encoder_layer').get_weights()[0])
weights = K.transpose(weights)
h = model.get_layer('encoder_layer').output
penalty_term = K.sum(((h * (1 - h))**2) * K.sum(weights**2, axis=1), axis=1)
Loss = MSE + (lambda * penalty_term)
return Loss
解剖变分自编码器
现在我们将看到另一种非常有趣的自编码器类型,称为变分自编码器(VAE)。与其他自编码器不同,VAE 是生成模型,意味着它们学习生成新数据,就像 GANs 一样。
假设我们有一个包含许多个体面部图像的数据集。当我们用这个数据集训练我们的变分自编码器时,它学会了生成新的逼真面部图像,这些图像在数据集中没有见过。由于其生成性质,变分自编码器有各种应用,包括生成图像、歌曲等。但是,什么使变分自编码器具有生成性质,它与其他自编码器有何不同?让我们在接下来的部分中学习。
正如我们在讨论 GAN 时学到的那样,要使模型具有生成性,它必须学习输入的分布。例如,假设我们有一个包含手写数字的数据集,如 MNIST 数据集。现在,为了生成新的手写数字,我们的模型必须学习数据集中数字的分布。学习数据集中数字的分布有助于 VAE 学习有用的属性,如数字的宽度、笔画、高度等。一旦模型在其分布中编码了这些属性,那么它就可以通过从学习到的分布中抽样来生成新的手写数字。
假设我们有一个包含人脸数据的数据集,那么学习数据集中人脸的分布有助于我们学习各种属性,如性别、面部表情、发色等。一旦模型学习并在其分布中编码了这些属性,那么它就可以通过从学习到的分布中抽样来生成新的人脸。
因此,在变分自编码器中,我们不直接将编码器的编码映射到潜在向量(瓶颈),而是将编码映射到一个分布中;通常是高斯分布。我们从这个分布中抽样一个潜在向量,然后将其馈送给解码器来重构图像。如下图所示,编码器将其编码映射到一个分布中,我们从该分布中抽样一个潜在向量,并将其馈送给解码器来重构图像:
高斯分布可以通过其均值和协方差矩阵来参数化。因此,我们可以让我们的编码器生成其编码,并将其映射到一个接近高斯分布的均值向量和标准差向量。现在,从这个分布中,我们抽样一个潜在向量并将其馈送给我们的解码器,解码器然后重构图像:
简而言之,编码器学习给定输入的理想属性,并将其编码成分布。我们从该分布中抽样一个潜在向量,并将潜在向量作为输入馈送给解码器,解码器然后生成从编码器分布中学习的图像。
在变分自编码器中,编码器也称为识别模型,解码器也称为生成模型。现在我们对变分自编码器有了直观的理解,接下来的部分中,我们将详细了解变分自编码器的工作原理。
变分推断
在继续之前,让我们熟悉一下符号:
-
让我们用
来表示输入数据集的分布,其中
表示在训练过程中将学习的网络参数。
-
我们用
表示潜在变量,通过从分布中采样来编码输入的所有属性。
-
表示输入
及其属性的联合分布,
。
-
表示潜在变量的分布。
使用贝叶斯定理,我们可以写出以下内容:
前述方程帮助我们计算输入数据集的概率分布。但问题在于计算 ,因为其计算是不可解的。因此,我们需要找到一种可行的方法来估计
。在这里,我们介绍一种称为变分推断的概念。
不直接推断 的分布,我们用另一个分布(例如高斯分布
)来近似它们。也就是说,我们使用
,这基本上是由
参数化的神经网络,来估计
的值:
-
基本上是我们的概率编码器;即,它们用来创建给定
的潜在向量 z。
-
是概率解码器;也就是说,它试图构建给定潜在向量
的输入
。
下图帮助你更好地理解符号及我们迄今为止看到的内容:
损失函数
我们刚刚学到,我们使用 来近似
。因此,
的估计值应接近
。由于这两者都是分布,我们使用 KL 散度来衡量
与
的差异,并且我们需要将这种差异最小化。
和
之间的 KL 散度如下所示:
由于我们知道,将其代入前述方程中,我们可以写出以下内容:
由于我们知道log (a/b) = log(a) - log(b),我们可以将前述方程重写为:
我们可以将从期望值中取出,因为它不依赖于
:
由于我们知道log(ab) = log (a) + log(b),我们可以将前述方程重写为:
我们知道和
之间的 KL 散度可以表示为:
将方程*(2)代入方程(1)*我们可以写出:
重新排列方程的左侧和右侧,我们可以写成以下内容:
重新排列项,我们的最终方程可以表示为:
上述方程意味着什么?
方程左侧也被称为变分下界或证据下界(ELBO)。左侧第一项表示我们希望最大化的输入x的分布,
表示估计和真实分布之间的 KL 散度。
损失函数可以写成以下内容:
在这个方程中,您会注意到以下内容:
-
意味着我们在最大化输入的分布;通过简单地添加一个负号,我们可以将最大化问题转化为最小化问题,因此我们可以写成
-
意味着我们在最大化估计和真实分布之间的 KL 散度,但我们想要将它们最小化,因此我们可以写成
来最小化 KL 散度
因此,我们的损失函数变为以下内容:
^()
如果你看这个方程式, 基本上意味着输入的重建,即解码器采用潜在向量
并重建输入
。
因此,我们的最终损失函数是重建损失和 KL 散度的总和:
简化的 KL 散度值如下所示:
因此,最小化上述损失函数意味着我们在最小化重建损失的同时,还在最小化估计和真实分布之间的 KL 散度。
重参数化技巧
我们在训练 VAE 时遇到了一个问题,即通过梯度下降。请记住,我们正在执行一个采样操作来生成潜在向量。由于采样操作不可微分,我们无法计算梯度。也就是说,在反向传播网络以最小化错误时,我们无法计算采样操作的梯度,如下图所示:
因此,为了应对这一问题,我们引入了一种称为重参数化技巧的新技巧。我们引入了一个称为epsilon的新参数,它是从单位高斯分布中随机采样的,具体如下所示:
现在我们可以重新定义我们的潜在向量 为:
重参数化技巧如下图所示:
因此,通过重参数化技巧,我们可以使用梯度下降算法训练 VAE。
使用 VAE 生成图像
现在我们已经理解了 VAE 模型的工作原理,在本节中,我们将学习如何使用 VAE 生成图像。
导入所需的库:
import warnings
warnings.filterwarnings('ignore')
import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import norm
from tensorflow.keras.layers import Input, Dense, Lambda
from tensorflow.keras.models import Model
from tensorflow.keras import backend as K
from tensorflow.keras import metrics
from tensorflow.keras.datasets import mnist
import tensorflow as tf
tf.logging.set_verbosity(tf.logging.ERROR)
准备数据集
加载 MNIST 数据集:
(x_train, _), (x_test, _) = mnist.load_data()
标准化数据集:
x_train = x_train.astype('float32') / 255.
x_test = x_test.astype('float32') / 255.
重塑数据集:
x_train = x_train.reshape((len(x_train), np.prod(x_train.shape[1:])))
x_test = x_test.reshape((len(x_test), np.prod(x_test.shape[1:])))
现在让我们定义一些重要的参数:
batch_size = 100
original_dim = 784
latent_dim = 2
intermediate_dim = 256
epochs = 50
epsilon_std = 1.0
定义编码器
定义输入:
x = Input(shape=(original_dim,))
编码器隐藏层:
h = Dense(intermediate_dim, activation='relu')(x)
计算均值和方差:
z_mean = Dense(latent_dim)(h)
z_log_var = Dense(latent_dim)(h)
定义采样操作
使用重参数化技巧定义采样操作,从编码器分布中采样潜在向量:
def sampling(args):
z_mean, z_log_var = args
epsilon = K.random_normal(shape=(K.shape(z_mean)[0], latent_dim), mean=0., stddev=epsilon_std)
return z_mean + K.exp(z_log_var / 2) * epsilon
从均值和方差中采样潜在向量 z:
z = Lambda(sampling, output_shape=(latent_dim,))([z_mean, z_log_var])
定义解码器
定义具有两层的解码器:
decoder_hidden = Dense(intermediate_dim, activation='relu')
decoder_reconstruct = Dense(original_dim, activation='sigmoid')
使用解码器重建图像,解码器将潜在向量 作为输入并返回重建的图像:
decoded = decoder_hidden(z)
reconstructed = decoder_reconstruct(decoded)
构建模型
我们按以下方式构建模型:
vae = Model(x, reconstructed)
定义重建损失:
Reconstruction_loss = original_dim * metrics.binary_crossentropy(x, reconstructed)
定义 KL 散度:
kl_divergence_loss = - 0.5 * K.sum(1 + z_log_var - K.square(z_mean) - K.exp(z_log_var), axis=-1)
因此,总损失可以定义为:
total_loss = K.mean(Reconstruction_loss + kl_divergence_loss)
添加损失并编译模型:
vae.add_loss(total_loss)
vae.compile(optimizer='rmsprop')
vae.summary()
训练模型:
vae.fit(x_train,
shuffle=True,
epochs=epochs,
batch_size=batch_size,
verbose=2,
validation_data=(x_test, None))
定义生成器
定义生成器从学习到的分布中取样并生成图像:
decoder_input = Input(shape=(latent_dim,))
_decoded = decoder_hidden(decoder_input)
_reconstructed = decoder_reconstruct(_decoded)
generator = Model(decoder_input, _reconstructed)
绘制生成的图像
现在让我们绘制由生成器生成的图像:
n = 7
digit_size = 28
figure = np.zeros((digit_size * n, digit_size * n))
grid_x = norm.ppf(np.linspace(0.05, 0.95, n))
grid_y = norm.ppf(np.linspace(0.05, 0.95, n))
for i, yi in enumerate(grid_x):
for j, xi in enumerate(grid_y):
z_sample = np.array([[xi, yi]])
x_decoded = generator.predict(z_sample)
digit = x_decoded[0].reshape(digit_size, digit_size)
figure[i * digit_size: (i + 1) * digit_size,
j * digit_size: (j + 1) * digit_size] = digit
plt.figure(figsize=(4, 4), dpi=100)
plt.imshow(figure, cmap='Greys_r')
plt.show()
以下是由生成器生成的图像的绘图:
总结
我们通过学习自编码器及其如何用于重构其自身输入来开始本章。我们探讨了卷积自编码器,其中我们使用卷积层和反卷积层进行编码和解码。随后,我们学习了稀疏自编码器,它只激活特定的神经元。然后,我们学习了另一种正则化自编码器类型,称为压缩自编码器,最后,我们学习了 VAE,这是一种生成自编码器模型。
在下一章中,我们将学习如何使用少量数据点来进行学习,使用 few-shot 学习算法。
问题
让我们通过回答以下问题来检验我们对自编码器的了解:
-
什么是自编码器?
-
自编码器的目标函数是什么?
-
卷积自编码器与普通自编码器有何不同?
-
什么是去噪自编码器?
-
如何计算神经元的平均激活?
-
定义了压缩自编码器的损失函数。
-
什么是 Frobenius 范数和雅可比矩阵?
进一步阅读
您也可以查看以下链接以获取更多信息:
-
稀疏自编码器 的笔记由 Andrew Ng 提供,
web.stanford.edu/class/cs294a/sparseAutoencoder_2011new.pdf -
压缩自编码器:特征提取期间的显式不变性 由 Salah Rifai 等人撰写,
www.icml-2011.org/papers/455_icmlpaper.pdf -
变分自编码器用于深度学习图像、标签和标题 由 Yunchen Pu 等人撰写,
papers.nips.cc/paper/6528-variational-autoencoder-for-deep-learning-of-images-labels-and-captions.pdf