说到计算机生成的图像肯定就会想到deep fake:将马变成的斑马或者生成一个不存在的猫。在图像生成方面GAN似乎成为了主流,但是尽管这些模型在生成逼真的图像方面取得了巨大成功,但他们的缺陷也是十分明显的,而且并不是生成图像的全部。自编码器(autoencoder)作为生成的图像的传统模型还没有过时并且还在发展,所以不要忘掉自编码器!
GAN 并不是您所需要的全部
当谈到计算机视觉中的生成建模时,几乎都会提到GAN 。使用GAN的开发了很多许多惊人的应用程序,并且可以在这些应用程序中生成高保真图像。但是GAN的缺点也十分明显:
1、训练不稳定,经常会出现梯度消失、模式崩溃问题(会生成相同的图像),这使得我们需要做大量的额外工作来为数据找到合适的架构。
2、GAN 很难反转(不可逆),这意味着没有简单的方法可以从生成的图像反推到产生这个图像的噪声输入。例如:如果使用可逆生成模型进行生成的图像的增强,可以直接获得生成图像的特定输入,然后在正确的方向上稍微扰动它这样就可以获得非常相似的图像,但是GAN做到这一点很麻烦。
3、GAN 不提供密度估计。也就是说可以生成图像但无法知道特定特征出现在其中的可能性有多大。例如:如果对于异常检测来说密度估计是至关重要的,如果有生成模型可以告诉我们一只可能的猫与一只不太可能的猫的样子,我们就可以将这些密度估计传递给下游的异常检测任务,但是GAN是无法提供这样的估计的。
自编码器 (AE) 是一种替代方案。它们相对快速且易于训练、可逆且具有概率性。AE 生成的图像的保真度可能还没有 GAN 的那么好,但这不是不使用他们的理由!
自编码器还没有过时
有人说:一旦 GAN 出现,自编码器就已经过时了。这在某种程度上是正确的, 但时代在进步GAN的出现让自编码器的发展有了更多的动力。在仔细地研究后人们已经意识到 GAN 的缺点并接受它们并不总是最适合的模型。,所以目前对自编码器继续进行更加深入的研究。
例如,一种被称为矢量量化变分自编码器 (Vector Quantized Variational AutoEncoder / VQ-VAE ) 的自回归 AE 声称可以生成与 GAN 的质量相匹配的图像,同时不会有 GAN 的已知缺点,例如模式崩溃和缺乏多样性等问题。
使用 VQ-VAE-2 生成多样化的高保真图像”(链接:arXiv:1906.00446)
在论文中,作者通过生成渔民图像将他们的 AE 模型与 DeepMind 的 BigGAN 进行了比较。可以看到 AE 生成的图像之间还是有多少变化的。
另外,在自编码器领域另一个令人兴奋的研究的例子是 VAE / GAN。这种混合模型使用 GAN 的鉴别器在典型的对抗训练中学到的知识来提高 AE 的生成能力。
“Autoencoding beyond pixels using a learned similarity metric”(arXiv:1512.09300)
在上图中作者使用他们的模型从学习的表示中重建一组图像,这是 GAN 无法做到的,因为GAN缺乏上面说过的的可逆性。从图上看重建看起来很不错。
虽然GAN很重要,但是自编码器还在以某种方式在图像生成中发挥作用( 自编码器可能还没被完全的开发),熟悉它们肯定是件好事。
在本文的下面部分,将介绍自编码器的工作原理、有哪些不同类型的自编码器以及如何使用它们。最后还将提供一些 TensorFlow 的代码。
使用自编码器进行表示学习
自编码器都是关于如何有效地表示数据的。他们的工作是找到一个高维输入的低维表示,在不损失内容的情况下重建原始输入。
从下图所示的quickdraw 数据集中获取“斧头”。图像为 28x28 灰度,这意味着它由 784 个像素组成。自编码器会找到从这个 784 维空间到 2D 空间的映射,这样压缩后的 ax 图像将仅由两个数字描述:地图上的 X 和 Y 坐标。接下来,仅知道 X-Y 坐标,自编码器将尝试仅从这两个值重建原始的 784 个像素。
自编码器学习其输入的低维度表示。
重建肯定不会是完美的,因为在压缩过程中不可避免地会丢失一些信息,但是我们的目标是希望它足以识别原始图像。在我们示例中的”地图“是有效表示数据的潜在空间。虽然我们使用 2D 进行说明,但实际上潜在空间通常会更大,但仍比输入图像小得多。
自编码器的工作是创建一个低维表示让它重建原始输入。这确保了这个潜在空间压缩了最相关的输入特征,并且没有噪声和对重建输入不重要的特征。
要点:自编码器的潜在空间压缩了现在相关的输入特征,并且没有噪声和冗余特征。
这个特点使得它在许多方面都具有吸引力。可以使用自编码器进行降维或特征提取(可以构建一个在数学上等同于主成分分析或 PCA 的自编码器,我们以前有个相应的文章,有兴趣的可以搜索参考)。所以可以在任何数据管道中用自编码器学习的低维度表示替换高维度数据。
自编码器还有许多其他应用。它们可用于对图像进行去噪:只需输入一张有噪声的图像,自编码器会重建原始的无噪声图像。它们还可用于自监督预训练,其中模型从大量未标记数据中学习图像特征,然后针对一小部分标记数据上的某些监督任务进行微调。最后自编码器可以用作生成模型,这将是本文的重点。
要点:自编码器可用于降维、特征提取、图像去噪、自监督学习和生成模型。
传统的自编码器 AE
这里使用 Google 游戏“Quick, Draw!”的玩家制作的手绘形状的 quickdraw 数据集构建一个简单的自编码器。为了方便演示, 我们将只使用三类图像:狗、猫和树。这是图像的示例。
如何构建一个自编码器呢?它需要由两部分组成:编码器,它接收输入图像并将其压缩为低维表示,以及解码器,它做相反的事情:从潜在表示产生原始大小的图像.
让我们从编码器开始。因为是处理图像所以在网络中使用卷积层。该模型将输入图像依次通过卷积层和最大池化层,以将它们压缩成低维表示。
encoder = tf.keras.models.Sequential([
tf.keras.layers.Reshape([28, 28, 1], input_shape=[28, 28]),
tf.keras.layers.Conv2D(
16, kernel_size=3, padding="same", activation="selu"
),
tf.keras.layers.MaxPool2D(pool_size=2),
tf.keras.layers.Conv2D(
32, kernel_size=3, padding="same", activation="selu"
),
tf.keras.layers.MaxPool2D(pool_size=2),
tf.keras.layers.Conv2D(
64, kernel_size=3, padding="same", activation="selu"
),
tf.keras.layers.MaxPool2D(pool_size=2)
])
这种特殊的架构基于 Aurélien Géron 在他的书中用于 FashionMNIST 数据集的架构(参见底部的来源)。这里使用 SELU 激活而不是 ReLU,是因为他比较新,效果也好😉
编码器最终输出 64 个特征图,每个特征图大小为 3x3,这就是对数据的低维表示。下面就需要一个解码器将这些表示处理成原始大小的图像。这里使用转置卷积(可以将其视为与常规卷积相反的操作)。转置卷积会放大图像,增加其高度和宽度,同时减少其深度或特征图的数量。
decoder = tf.keras.models.Sequential([
tf.keras.layers.Conv2DTranspose(
32, kernel_size=3, strides=2, padding="valid", activation="selu", input_shape=[3, 3, 64]
),
tf.keras.layers.Conv2DTranspose(
16, kernel_size=3, strides=2, padding="same", activation="selu"
),
tf.keras.layers.Conv2DTranspose(
1, kernel_size=3, strides=2, padding="same", activation="sigmoid"
),
tf.keras.layers.Reshape([28, 28])
])
剩下要做的就是将编码器与解码器连接起来,并将它们作为一个完整的自编码器进行联合训练。使用二元交叉熵损失对模型进行了 20 个 epoch 的训练,代码如下:
ae = tf.keras.models.Sequential([encoder, decoder])
ae.compile(
loss="binary_crossentropy",
optimizer=tf.keras.optimizers.Adam(learning_rate=0.001),
metrics=["accuracy"]
)
history = ae.fit(
X_train, X_train,
epochs=20,
validation_data=(X_val, X_val)
)
损失函数选择来说:二元交叉熵和RMSE都可以被用作损失函数, 两者的主要区别在于二元交叉熵对大误差的惩罚更强,这可以将重建图像的像素值推入平均幅度,但是这反过来又会使重建的图像不那么生动。因为这个数据集是灰度图像,所以损失函数的选择不会产生任何有意义的差异。
下面看一下测试集中的一些图像,以及自编码器重建它们的效果如何。
测试集的原始图像(上)与它们的重建图像(下)。
看起来不错,但是一些细节模糊(这是自编码器的缺陷,也是GAN的优势),但整体重建精度似乎相当不错。另一种可视化自编码器所学内容的方法是将一些测试图像仅传递给编码器。这将产生它们的潜在表示,本例(3, 3, 64) 。然后使用降维算法(例如 t-SNE)将它们映射到二维并绘制散点图,通过它们的标签(猫、狗或树)为点着色,如下图所示:
可以清楚地看到,树与其他图像分离良好而猫和狗则有点混杂。注意底部的大蓝色区域,这些是带有胡须的猫头的图像这些并没有与狗混淆。但是在图的的上半部分都是从动物的侧面,这使得区分猫和狗变得更加困难。这里一个非常值得关注的事情是,自编码器在没有给出标签的情况下了解了多少图像类别!(上面说到的自监督学习)
要点:自编码器可以在没有标签的情况下学习很多关于图像分类的知识。
传统的自编码器模型似乎已经学会了数据的有意义的潜在表示。下面让我们回到本文的主题:它可以作为生成模型吗?
传统自编码器作为生成模型
首先明确一下我们对生成模型的期望:希望能够选择潜在空间中的任何随机点,将其通过解码器获得逼真的图像。最重要的是,在潜在空间中选择不同的随机点应该会产生不同的生成图像,这些图像应该涵盖模型看到的所有类型的数据:猫、狗和树。
从潜在空间采样
当我们在潜在空间中选择一个随机点时,第一个问题就出现了:在这种情况下,“随机”是什么意思?它应该来自正态分布还是均匀分布?分布应该如何参数化?下图显示了对测试数据样本进行编码后潜在空间值的概率密度。
除此以外,我还计算了一些汇总统计数据:最小潜在空间值为 -1.76,最大值为 22.35。对于随机点采样,让潜在空间以零为中心对称中心化会容易得多,或者说至少以某种方式是有界的,需要一个最大值和最小值。
要点:潜在空间值形成不规则的、无界的分布,会使随机点采样变得困难。
图像多样性
另一个问题涉及潜在空间中各个类别的代表区域,这会影响生成图像的多样性。
模型的潜在空间是 3x3x64,它是 576 维的无法可视化。为了便于解释可以尝试对一个维度进行 3D 切片,其形状为 3x1x1。只考虑此切片时,每个图像在潜在空间中由 3D 矢量表示可以将其可视化为散点图。这是测试数据样本的图:
蓝色点云分布在比红色和绿色云小得多的体积上。这意味着如果要从这个空间中随机抽取一个点,最终得到猫或狗的可能性要比得到树的可能性大得多。在极端情况下,考虑到潜在空间的所有 576 个维度,可能永远不会对树进行采样,这违背了对生成模型能够覆盖它所看到的数据的整个空间的要求。
要点:不同图像类别的潜在表示可能在大小上有所不同,导致模型生成某些类别的频率比其他类别高得多。
红色和绿色点云中向上突出的尖峰。在这个尖峰内部存在一些图像的潜在表示。但如果从那里向旁边移动,在尖刺旁边的正上方一个点取样呢?能得出真实的图像吗?
潜在空间中的有意义区域
在潜在空间的 3D 子空间中,图像嵌入通常是良好聚类的——可能除了点云顶部的红绿 尖峰 之外。但是随着我们添加更多的维度,嵌入式图像之间会出现更多的空白空间。这使得整个 3x3x64 的潜在空间充满了真空。当从其中随机采样一个点时,很可能会从任何特定图像中得到一个远离(在现在的维度上)的点。如果通过解码器传递这些随机选择的点,我们会得到什么?答案是得不到任何的形状。
猫和狗之间的采样不应该产生一个耳朵和胡须松软的生物吗?
传统自编码器学习的潜在空间不是连续的,所以该空间中的点之间的含义没有平滑的过渡。并且即使是一个小的扰动点也可能会致垃圾输出。
要点:传统的自编码器学习的潜在空间不是连续的。
使用传统自编码器作为生成模型存在三个问题:不知道如何从一个不规则的、无界的空间中采样,一些类可能在潜空间中被过度表示,学习空间是不连续的,这使得很难找到一个点将解码成一个良好的图像。所以这时候变分自编码器出现了。
变分自编码器 VAE
变分自编码器(Variational autoencoder)或称 VAE,通过引入随机性和约束潜在空间以便更容易从中采样来解决上面讨论的问题。
要点:变分自编码器将随机性引入模型并限制潜在空间。
要将传统自编码器转换为变分自编码器,只需要调整编码器部分和损失函数。让我们从第一步开始。
变分编码器
变分编码器不是将输入图像映射到潜在空间中的一个点,而是将其映射到一个分布中,准确地说是多元正态分布(multivariate normal distribution)。
多元正态分布是将单变量正态分布扩展到更多维度。就像单变量正态分布由两个参数描述:均值和方差,多元正态分布由两个参数向量描述,每个参数的长度等于维数。例如,2D 法线将有一个包含两个均值的向量和一个包含两个方差的向量。如果分布的许多维度是相关的,则会出现额外的协方差参数,但在 VAE 中,假设所有维度都是独立的,这样所有协方差为零。
为了将输入图像编码为潜在空间中的低维度表示,将从多元正态分布中对其进行采样,其参数(均值和方差)将由编码器学习。
这样潜在空间将用两个向量来描述:均值向量和方差向量。本文的例子中将这两个向量都设为 576 维,以匹配之前构建的编码器,后者编码为 3x3x64 = 576 维空间。实际上可以重用上面的编码器代码。只需展平它的输出并将两个向量附加到它上面。
vanilla_encoder = tf.keras.models.clone_model(encoder)
encoder_inputs = tf.keras.layers.Input(shape=[28, 28])
z = vanilla_encoder(encoder_inputs)
z = tf.keras.layers.Flatten()(z)
codings_mean = tf.keras.layers.Dense(576)(z)
codings_log_var = tf.keras.layers.Dense(576)(z)
codings = Sampling()([codings_mean, codings_log_var])
var_encoder = tf.keras.models.Model(
inputs=[encoder_inputs],
outputs=[codings_mean, codings_log_var, codings]
)
这里只有两件事需要详细说明:
1、正如可能从变量名称中猜到的那样,使用方差的对数来描述正态分布,而不是按原样描述方差。这是因为方差需要为正,而对数方差可以是任何值。
2、编码器使用自定义采样层,该层根据均值和对数变量从多元法线中采样一个点。下面就是代码:
class Sampling(tf.keras.layers.Layer):
def call(self, inputs):
mean, log_var = inputs
epsilon = K.random_normal(tf.shape(log_var))
return mean + K.exp(log_var / 2) * epsilon
为什么变分编码器可以工作
与传统编码器相比,VAE不将输入映射到一个确定性点,而将其映射到某个空间中的一个随机点。为什么这个更好呢?
对于一个相同的图像,每次都会在潜在空间中得到一个稍微不同的点(尽管它们都在均值附近)。这使得 VAE 了解该邻域中的所有点在解码时都应该产生类似的输出。这确保了潜在空间是连续的!
要点:编码器中的随机化迫使潜在空间是连续的。
变分解码器
VAE 的解码器不需要太多更改,直接可以重用以前的代码。
vanilla_decoder = tf.keras.models.clone_model(decoder)
decoder_inputs = tf.keras.layers.Input(shape=[576])
x = tf.keras.layers.Reshape([3, 3, 64])(decoder_inputs)
decoder_outputs = vanilla_decoder(x)
var_decoder = tf.keras.models.Model(
inputs=[decoder_inputs],
outputs=[decoder_outputs]
)
唯一的区别是现在编码器的输出或潜在空间是一维向量而不是 3D 张量。所以只需添加一个重塑层就可以了。现在可以将变分编码器和解码器组合到 VAE 模型中。
_, _, codings = var_encoder(encoder_inputs)
reconstructions = var_decoder(codings)
vae = tf.keras.models.Model(
inputs=[encoder_inputs],
outputs=[reconstructions]
)
变分损失函数
在传统自编码器中,使用了二元交叉熵损失,并提到均方根误差可能是一种替代方法。在 VAE 中损失函数是需要扩展得,因为交叉熵或 RMSE 损失是一种重建损失——它会惩罚模型以产生与原始输入不同的重建。
在 VAE 中在损失函数中增加了KL 散度,惩罚模型学习与标准正态有很大不同的多元正态分布。KL 散度是衡量两个分布差异程度的指标,在此可以衡量标准正态分布与模型学习的分布之间的差异。也就是说:如果均值不为零且方差不为 1,则会产生损失。
latent_loss = -0.5 * \
K.sum(1 + codings_log_var - K.exp(codings_log_var) - \
K.square(codings_mean),
axis=-1)
vae.add_loss(K.mean(latent_loss) / (28 * 28))
vae.compile(
loss="binary_crossentropy",
optimizer=tf.keras.optimizers.Adam(learning_rate=0.001),
metrics=["accuracy"]
)
latent_loss 的公式就是 KL-divergence 公式,并且在这种特殊情况下得到简化:目标分布是标准正态分布并且两者都没有零协方差。另外就是需要将其缩放到输入图像的大小,以确保它与重建损失具有相似的比例并且不会占主导地位。既然不是主导地位,为什么我们要把这个 KL 部分加到损失中呢?
1、它使潜在空间中的随机点采样变得简单。我们可以从标准法线中取样,并确保该空间对模型有意义。
2、由于标准法线是圆形的并且围绕其平均值对称,因此潜在空间中存在间隙的风险较小,也就是说解码器产生无效的图像的概率会小。
通过以上方式,VAE克服了传统自编码器在图像生成方面的所有三个缺点。现在训练一下看看效果。
history = vae.fit(
X_train, X_train,
epochs=100,
batch_size=128,
validation_data=(X_val, X_val),
)
变分自编码器的分析
原始图像和它们的重建图像。
后者可能看更模糊,这是意料之中的,毕竟我们调整了损失函数:不仅关注重建精度,还关注产生有意义的潜在空间。
图像之间的变形
先来验证变分自编码器学习到的潜在空间确实是连续的、行为良好且有意义的,那就是选择两个图像并在它们之间变形。让我们以这只猫和这棵树为例。
对它们进行编码以获得它们的隐藏表示,并在它们之间进行线性插值。然后将沿插值线的每个点传递给解码器,这样可以在猫和树之间生成图像。
cat = var_encoder(X_train[5930, :, :].reshape(1, 28, 28))[0].numpy()
tree = var_encoder(X_train[17397, :, :].reshape(1, 28, 28))[0].numpy()
linfit = interp1d([1, 10], np.vstack([cat, tree]), axis=0)
将两个潜在表示堆叠在一个形状为 2x576 的矩阵中,并应用 scipy 的线性插值函数,如果需要调整,可以修改 linfit ([i + 1 for i in range (10)]) 来获得中间插值。
仔细看看猫的嘴是如何变成树干的。以类似的方式,还可以将另猫变成狗。注意猫的尖耳朵是如何逐渐变成狗的松软耳朵的。
这个有趣的实验表明,变分自编码器学习的潜在空间是连续的,并确保点之间的平滑过渡。
要点:VAE 潜在空间是连续的,允许在图像之间生成有意义的插值。
如果潜在空间是连续且有意义的,我们应该能够对图像进行算术运算。
考虑这两只猫(图片是重建而不是原始图像)。
如果从左边有胡须的猫中减去右边的无胡须猫,我们会得到什么?减法必须发生在潜在空间中。
cat_1 = var_encoder(X_train[19015, :, :].reshape(1, 28, 28))[0].numpy()
cat_2 = var_encoder(X_train[7685, :, :].reshape(1, 28, 28))[0].numpy()
result = var_decoder(cat_1 - cat_2)
结果类似于胡须?还真有点像
总结
本文中已经介绍了自编码器如何学习数据的低维表示,以及这些潜在表示对于新图像的生成是如何不完美的,至少在传统自编码器的情况下:它们学习的空间难以采样且不连续。
还介绍了变分自编码器如何通过向编码器引入随机性并增强损失函数来强制学习连续且有意义的潜在空间来缓解这些问题,从而允许在图像之间进行算术和变形。
上面讨论的示例是在具有现成架构的简单数据集上训练的。想象一下实际应用得时候变分自编码器有多么强大!
引用:
- Geron A., 2019, 2nd edition, Hands-On Machine Learning with Scikit-Learn and TensorFlow: Concepts, Tools, and Techniques to Build Intelligent Systems, O’Reilly
- Foster D., 2019, Generative Deep Learning. Teaching Machines to Paint, Write, Compose and Play, O’Reilly
- https://www.overfit.cn/post/bc1df57b1f1a499c996e92875ec48923
本文作者:Michał Oleszak
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。