机器学习:自编码器概述及实验

在今天的文章中,我想简单介绍一下一种名为自编码器的神经网络架构类型。这篇文章的目标是机器学习的初学者,他们希望对自编码器有一些基本的了解,以及为什么它们如此有用。

上下文

自编码器的结构是接受输入,将该输入转换为另一种表示形式,即输入的嵌入(embedding)。通过这种嵌入,它的目的是尽可能精确地重建原始输入。它会尝试复制输入。创建这种嵌入的自编码器的层称为编码器,试图将嵌入重建到原始输入中的层称为解码器。通常自编码器的限制方式只允许它们近似地复制。因为模型被迫优先考虑应该复制输入的哪些方面,所以它通常会学习数据的有用属性。

自编码器使用中间表示x_encoded = f_encode(x)描述输入x到输出x的非线性映射,也称为嵌入。嵌入通常表示为h(h表示隐藏)。在训练期间,编码器学习x到x_encoded的非线性映射。另一方面,解码器学习从x_encoded到原始空间的非线性映射。训练的目的是减少损失。这种损失描述了自编码器试图达到的目标。当我们的目标是尽可能精确地重构输入时,通常使用两种主要类型的损失函数:均方误差和kullback - leibler(KL)散度。

均方误差(MSE)定义为我们的网络输出与ground truth之间的平方差的平均值。MSE 可以定义为

KL散度的概念最初来源于信息论,描述了两个概率分布p和q之间的相对熵,由于KL散度是非负的,衡量的是两个分布之间的差异,所以通常被理解为测量这些分布之间的某种距离。

KL散度具有许多有用的属性,最值得注意的是它是非负的。当且仅当pp和q在离散变量的情况下是相同的分布,或者在连续变量的情况下几乎处处相等时,KL散度为0 。它被定义为:

在机器学习的上下文中,最小化KL散度意味着使自编码器从与分布类似的分布中对其输出进行采样,这是自编码器的期望属性。

自编码器

自编码器有许多不同的风格。出于本文的目的,我们将仅讨论自编码器的最重要的概念和想法。你可能会在遇到的大多数自动编码是undercomplete自动编码。这意味着输入的压缩表示可以保存比输入更少的信息。如果您的输入具有N维,则自编码器的某些隐藏层只有X < N维,您的自编码器undercomplete。为什么希望在隐藏层中保存的信息少于输入可能包含的信息呢?该想法是限制编码器可以放入编码表示中的信息量迫使其仅关注输入内的相关和判别信息,因为这允许解码器尽可能最好地重建输入。Undercomplete自编码器将信息分解为最基本的位。它是降维的一种形式。

现在,让我们讨论一些您可能会遇到的自编码器:

Vanilla自编码器

可以使用输入层,隐藏层和输出层来定义自编码器的最基本示例:

一个简单的自编码器

输入层通常具有与输出层相同的尺寸,因为我们尝试重建输入的内容,而隐藏层具有输入或输出层的较小数量的维度。

稀疏自编码器(Sparse Autoencoder)

然而,根据编码方案的目的,在损失函数中添加一个需要满足的附加项可能是有用的。

正如其名称所示,稀疏自编码器强制实现嵌入变量的稀疏性。这可以通过嵌入层h上的稀疏惩罚Ω(h)来实现。

运算符L表示输入和输出之间的任意距离度量(即,MSE或KL-发散)。稀疏性惩罚可以表示为隐藏层权重的L 1范数:

具有scaling参数λ。增强稀疏性是正则化的一种形式,可以提高自编码器的泛化能力。

去噪自编码器(Denoising Autoencoder)

顾名思义,去噪自编码器能够稳健地去除图像中的噪声。它如何实现这个属性?它发现在输入中(在合理的信噪比范围内)对噪声具有一定不变性的特征向量。

通过修改vanilla自编码器的损失函数,可以非常容易地构造去噪自编码器。我们不是计算原始输入x和重构输入x之间的误差,而是计算原始输入和输入x的重建之间的误差,输入x被某种形式的噪声破坏:

去噪自编码器学习撤销这种损坏,而不是简单地复制他们的输入。

去噪图像

收缩自编码器(Contractive Autoencoder)

一个控制自编码器是稀疏自编码器的另一个子类型(我们对重建损失施加了额外的约束)。对于这种类型的自编码器,我们惩罚嵌入层的权重

运算符∇表示Nabla运算符,表示梯度。具体来说,我们惩罚隐藏层激活的大梯度h_i与输入x。但这种约束有什么目的呢?

简单的说,它允许输入x的无穷小变化对嵌入变量没有任何影响。如果对输入图像的像素强度进行小的改变,我们不希望对嵌入变量进行任何更改。鼓励将输入点的局部邻域映射到输出点的较小的局部邻域。

问这个有用的是什么?CAE的目标是学习高维输入空间中数据的流形结构。例如,应用于图像的CAE应该学习切向量,该向量向量显示了随着图像中的对象逐渐改变姿态,图像是如何变化的。在标准损失函数中不会强调该属性。

变分自编码器(Variational Autoencoder)

变分自编码器(VAE)为其输入数据学习一个潜在的变量模型,因此,与让神经网络学习任意函数不同,您学习的是对数据建模的概率分布参数。如果您从这个分布中采样点,您可以生成新的输入数据样本:VAE是一个“生成模型”。

与“普通”自编码器相比,VAE不将样本转换为一个参数(嵌入表示),而是在两个参数z_μ和z_σ中,它描述了一个潜在正态分布的均值和标准差,该正态分布被假设用来生成VAE所训练的数据。

通过两个损失项训练模型的参数:重建损失迫使解码的样本与初始输入匹配(就像我们之前的自编码器一样),以及学习后的潜在分布与先验分布之间的KL散度,作为正则化项。

实验

2D-Manifold Embedding

现在让我们看看如何将数据嵌入到某些潜在维度中。在第一个实验中,我们将努力做一些非常简单的事情。我们首先创建一个超级单调的数据集,它由许多不同高度和宽度的随机块的不同图像组成,我们称之为块图像数据集。

让我们在80000个这些块图像上训练只有两个潜在维度的VAE,看看会发生什么。我选择仅使用两个潜在维度,因为每个图像可以通过其潜在嵌入向量在2-D平面中的位置来可视化。

下图显示了二维平面中的哪个特征向量对应于哪个块图像。块图像绘制在其特征向量位于二维平面的位置。

在均匀网格上对2-D潜在特征进行采样

很明显,autoencoder能够找到对我们的数据集很有意义的映射。回想一下,每个输入数据(单个图像)高度⋅宽度⋅通道= 28⋅28⋅1 = 784维度。自编码器能够将输入的维度减少到只有两个维度,而不会丢失大量信息,因为输出在视觉上几乎无法与输入区分开。这种惊人的重建质量是可能的,因为每个输入图像都很容易描述并且不包含很多信息。每个白色块只能由两个参数描述:高度和宽度。甚至每个块的中心都没有被参数化,因为每个块恰好位于每个图像的中心。

实现的Python代码:

from __future__ import absolute_importfrom __future__ import divisionfrom __future__ import print_functionfrom tensorflow.keras.layers import Lambda, Input, Densefrom tensorflow.keras.models import Modelfrom tensorflow.keras.losses import mse, binary_crossentropyfrom tensorflow.keras.utils import plot_modelfrom tensorflow.keras import backend as Kfrom pathlib2 import Pathfrom matplotlib import cmimport numpy as npimport matplotlib.pyplot as pltimport argparseimport osfrom skimage import drawdef make_images(): N = 100000 # so many samples dims = [28, 28] images = np.zeros([N, dims[0], dims[1]]) images = [] for i in range(N): im = np.zeros(dims, dtype=np.float32) height = np.random.randint(5, 23) width = np.random.randint(5, 23) beginx = (28 - height) // 2 beginy = (28 - width) // 2 im[beginx:beginx+height, beginy:beginy+width] = 1.0 images.append(im) images = np.array(images) # Super simple split into training- and test set return images[0:80000, :, :], images[80000:, :, :]# reparameterization trick# instead of sampling from Q(z|X), sample eps = N(0,I)# z = z_mean + sqrt(var)*epsdef sampling(args): '''Reparameterization trick by sampling fr an isotropic unit Gaussian. # Arguments args (tensor): mean and log of variance of Q(z|X) # Returns z (tensor): sampled latent vector ''' z_mean, z_log_var = args batch = K.shape(z_mean)[0] dim = K.int_shape(z_mean)[1] # by default, random_normal has mean=0 and std=1.0 epsilon = K.random_normal(shape=(batch, dim)) return z_mean + K.exp(0.5 * z_log_var) * epsilondef plot_results(models, data, batch_size=128, model_name='vae_mnist'): '''Plots labels and MNIST digits as function of 2-dim latent vector # Arguments models (tuple): encoder and decoder models data (tuple): test data and label batch_size (int): prediction batch size model_name (string): which model is using this function ''' encoder, decoder = models x_test, _ = data Path(model_name).mkdir(exist_ok=True) filename = os.path.join(model_name, 'vae_mean.png') # display a 2D plot of the digit classes in the latent space z_mean, _, _ = encoder.predict(x_test, batch_size=batch_size) plt.figure(figsize=(12, 10)) plt.scatter(z_mean[:, 0], z_mean[:, 1]) #plt.colorbar() plt.xlabel('z[0]') plt.ylabel('z[1]') plt.savefig(filename) plt.show() filename = os.path.join(model_name, 'digits_over_latent.png') # display a 30x30 2D manifold of digits n = 30 digit_size = 28 figure = np.zeros((digit_size * n, digit_size * n)) # linearly spaced coordinates corresponding to the 2D plot # of digit classes in the latent space grid_x = np.linspace(-3, 3, n) grid_y = np.linspace(-3, 3, n)[::-1] for i, yi in enumerate(grid_y): for j, xi in enumerate(grid_x): z_sample = np.array([[xi, yi]]) x_decoded = decoder.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=(10, 10)) start_range = digit_size // 2 end_range = n * digit_size + start_range + 1 pixel_range = np.arange(start_range, end_range, digit_size) sample_range_x = np.round(grid_x, 1) sample_range_y = np.round(grid_y, 1) plt.xticks(pixel_range, sample_range_x) plt.yticks(pixel_range, sample_range_y) plt.xlabel('z[0]') plt.ylabel('z[1]') plt.imshow(figure, cmap='Greys_r') plt.savefig(filename) plt.show()x_train, x_test = make_images()image_size = x_train.shape[1]original_dim = image_size * image_sizex_train = np.reshape(x_train, [-1, original_dim])x_test = np.reshape(x_test, [-1, original_dim])# network parametersinput_shape = (original_dim, )intermediate_dim = 512batch_size = 128latent_dim = 2epochs = 50# VAE model = encoder + decoder# build encoder modelinputs = Input(shape=input_shape, name='encoder_input')x = Dense(intermediate_dim, activation='relu')(inputs)z_mean = Dense(latent_dim, name='z_mean')(x)z_log_var = Dense(latent_dim, name='z_log_var')(x)# use reparameterization trick to push the sampling out as input# note that 'output_shape' isn't necessary with the TensorFlow backendz = Lambda(sampling, output_shape=(latent_dim,), name='z')([z_mean, z_log_var])# instantiate encoder modelencoder = Model(inputs, [z_mean, z_log_var, z], name='encoder')encoder.summary()# plot_model(encoder, to_file='vae_mlp_encoder.png', show_shapes=True)# build decoder modellatent_inputs = Input(shape=(latent_dim,), name='z_sampling')x = Dense(intermediate_dim, activation='relu')(latent_inputs)outputs = Dense(original_dim, activation='sigmoid')(x)# instantiate decoder modeldecoder = Model(latent_inputs, outputs, name='decoder')decoder.summary()# plot_model(decoder, to_file='vae_mlp_decoder.png', show_shapes=True)# instantiate VAE modeloutputs = decoder(encoder(inputs)[2])vae = Model(inputs, outputs, name='vae_mlp')if __name__ == '__main__': parser = argparse.ArgumentParser() help_ = 'Load h5 model trained weights' parser.add_argument('-w', '--weights', help=help_) help_ = 'Use mse loss instead of binary cross entropy (default)' parser.add_argument('-m', '--mse', help=help_, action='store_true') args = parser.parse_args() models = (encoder, decoder) data = (x_test, []) # VAE loss = mse_loss or xent_loss + kl_loss if args.mse: reconstruction_loss = mse(inputs, outputs) else: reconstruction_loss = binary_crossentropy(inputs, outputs) reconstruction_loss *= original_dim kl_loss = 1 + z_log_var - K.square(z_mean) - K.exp(z_log_var) kl_loss = K.sum(kl_loss, axis=-1) kl_loss *= -0.5 vae_loss = K.mean(reconstruction_loss + kl_loss) vae.add_loss(vae_loss) vae.compile(optimizer='adam') vae.summary() # plot_model(vae, # to_file='vae_mlp.png', # show_shapes=True) if args.weights: vae.load_weights(args.weights) else: # train the autoencoder vae.fit(x_train, epochs=epochs, batch_size=batch_size, validation_data=(x_test, None)) vae.save_weights('vae_mlp_mnist.h5') plot_results(models, data, batch_size=batch_size, model_name='vae_mlp')

类似的图像检索

虽然嵌入这些简单的块似乎是一个不错的噱头,但现在让我们看看autoencoder在真实数据集(Fashion MNIST)上的实际执行情况。我们的目标是找出输入图像的嵌入向量的描述性。自编码器允许我们比较视觉图像的相似性(通过比较各自的嵌入或由自编码器创建的特征的相似性)。

由于Fashion MNIST图像比我们上一次迷你实验的块图像信息密集得多,我们假设我们需要更多潜在变量来表达每个训练图像的主旨。我选择了一个具有128个潜在维度的不同自编码器架构。

我们的想法是为查询图像创建一个特征向量,我们希望从数据库获得类似的结果。下面我们可以看到(非常低分辨率)牛仔裤的示例性查询图像。

使用我们在Fashion MNIST数据集上训练的自编码器,我们希望检索与嵌入空间中最接近查询图像特征的特征相对应的图像。但是我们如何比较两个向量的接近程度呢?对于这种任务,通常使用向量之间的余弦距离作为距离度量。

以下是要素空间中查询图像的四个最相似的:

让我们再试一次新的查询图片:

以下是四个最相似的:

自编码器绝对能够编码嵌入层中每个输入图像的相关信息。

注意:您应该记住,自编码器未经过任何图像标签的训练。它并不“知道”这些是衬衫的图像。仅知道所有这些图像的抽象特征大致相似并且高度描述实际图像内容。

实现的Python示例代码如下:

from keras.layers import Input, Densefrom keras.models import Modelimport matplotlib.pyplot as plt import numpy as np from sklearn.neighbors import LSHForestimport matplotlib.pyplot as pltfrom keras.datasets import fashion_mnist# Autoencoder model definitioninput_img = Input(shape=(784,))encoded = Dense(128, activation='relu')(input_img)encoded = Dense(64, activation='relu')(encoded)encoded = Dense(32, activation='relu', name='encoded')(encoded)decoded = Dense(64, activation='relu')(encoded)decoded = Dense(128, activation='relu')(decoded)decoded = Dense(784, activation='sigmoid')(decoded)autoencoder = Model(input_img, decoded)autoencoder.compile(optimizer='adadelta', loss='binary_crossentropy')# Load fashion MNIST dataset(x_train, _), (x_test, _) = fashion_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:])))# Train autoencoder on dataautoencoder.fit(x_train, x_train, epochs=1, batch_size=128, shuffle=True, validation_data=(x_test, x_test), callbacks=[])# Define Enocder as new modellayer_name = 'encoded'encoder = Model(inputs=autoencoder.input, outputs=autoencoder.get_layer(layer_name).output)# Generate feature vectors of test datasetx_test_encoded = encoder.predict(x_test)x_test = np.reshape(x_test, [-1, 28, 28])# create Local Sensitivity hashing instance for fast neighborhood searchlshf = LSHForest(random_state=42) lshf.fit(x_test_encoded) # Random index of query image from test setrandom_query = np.random.randint(0, 1000)query_features = np.expand_dims(x_test_encoded[random_query, :], axis=0)distances, indices = lshf.kneighbors(query_features, n_neighbors=5)plt.imshow(x_test[random_query, :, :])plt.title('Query image')plt.gray()plt.show()for i in range(1, 5): ax = plt.subplot(1, 4, i) plt.imshow(x_test[indices[0][i], :, :]) plt.gray() plt.title('Distance = ' + str(distances[0][i])) ax.get_xaxis().set_visible(False) ax.get_yaxis().set_visible(False)plt.show()

结论

我们已经看到了如何构建自编码器以及在过去几年中已经提出了哪种类型的自编码器。

自编码器在密集的小特征向量中编码高级图像内容信息的能力使它们对于无人监督的预训练非常有用。我们可以从完全无监督的输入数据中自动提取非常有用的特征向量。稍后我们可以使用这些特征向量来训练具有这些特征的现成分类器并观察极具竞争力的结果。

这个方面对于没有很多标记数据但是非常标记的数据的学习任务特别有用。

(0)

相关推荐