自编码器是一种主要用于实现表示学习的模型。表示学习是一种深度学习任务,专注于生成紧凑且具有代表性的特征,用于表示任何单一数据样本,无论是图像、文本、音频、视频还是多模态数据。经过某种形式的表示学习后,模型将能够将输入映射到更具代表性的特征,这些特征可以用于区分不同的样本输入。获得的表示将存在于一个潜在空间中,在该空间中,不同的输入样本将共同存在。这些表示也被称为嵌入。自编码器的应用将与表示学习的应用紧密相关,某些应用包括为后续的监督学习任务生成预测特征、在实际数据中进行样本比较与对比,以及执行有效的样本识别。
需要注意的是,自编码器并不是执行表示学习的唯一方式。表示学习的主题将在第8章《探索监督深度学习》和第9章《探索无监督深度学习》中进一步讨论。
现在,我们知道自编码器学习生成独特的表示,或者换句话说,学习生成嵌入。那么它的架构是怎样的呢?让我们先了解一种标准架构,然后探讨一些有用的改进。
在本章中,我们将涵盖以下主题:
- 解码标准自编码器
- 探索自编码器的变体
- 构建CNN自编码器
技术要求
本章包括一些Python编程语言中的实际实现。要完成本章内容,您需要一台安装了以下库的计算机:
- pandas
- Matplotlib
- Seaborn
- scikit-learn
- NumPy
- Keras
- PyTorch
代码文件可以在GitHub上找到:github.com/PacktPublis…
解码标准自编码器
自编码器更像是一个概念,而不是一个实际的神经网络架构。这是因为它们可以基于不同的基础神经网络层。当处理图像时,通常构建CNN自编码器;当处理文本时,可能需要构建RNN自编码器。当处理包含图像、文本、音频、数值和类别数据的多模态数据集时,你会使用不同层的组合作为基础。自编码器主要由三个组成部分构成,分别是编码器、瓶颈层和解码器。图5.1展示了这一结构。
标准自编码器的编码器通常接收高维数据,并将其压缩到比原始数据维度更小的任意尺度,这将产生所谓的瓶颈表示,其中它与瓶颈相绑定,表示一个紧凑的表示,没有任何无用的信息。然后,瓶颈组件会被传递到解码器,在解码器中,它会使用与编码器所使用的尺度相反的方式扩展维度,最终产生与输入数据相同维度的输出。
注意
编码器和解码器结构不仅限于自编码器,还在其他架构中使用,例如transformers。
区别在于瓶颈组件,它承载了代表性特征。瓶颈通常会被压缩为较小的维度,但有时也可以被扩大,以容纳更多代表性特征,从而增强预测能力。
一般来说,自编码器是被训练来重构输入数据的。自编码器模型的训练过程包括比较生成的输出数据与输入数据之间的距离。在经过优化以生成输入数据后,当模型能够完全重构原始输入数据时,可以认为瓶颈具有比原始输入数据本身更紧凑和总结性的表示。然后,这种紧凑的表示可以被用来执行其他任务,如样本识别,或者甚至可以用来节省空间,通过存储较小的瓶颈特征,而不是原始的大型输入数据。编码器和解码器不局限于单一层次, 可以使用多个层来定义。然而,标准自编码器只有一个瓶颈特征,这也被称为代码或潜在特征。
现在,让我们探索自编码器的不同变体。
探索自编码器的变体
对于表格数据,网络结构可以非常简单。它仅仅使用一个多层感知机(MLP),其中多个全连接层逐步减少编码器的特征数量,而解码器则使用多个全连接层,逐步增加数据输出,直到与输入的维度和大小相同。
对于时间序列或顺序数据,可以使用基于RNN的自编码器。关于基于RNN的自编码器,最常引用的研究项目之一是使用基于LSTM的编码器和解码器的版本。该研究论文名为《使用神经网络进行序列到序列学习》(Sequence to Sequence Learning with Neural Networks),作者是Ilya Sutskever、Oriol Vinyals和Quoc V. Le(arxiv.org/abs/1409.32…)。该研究与其通过堆叠编码器LSTM和解码器LSTM,使用每个LSTM单元的隐藏状态输出序列垂直排列不同,解码器层顺序地继续编码器LSTM的序列流,并以反向顺序输出重构的输入。还使用了一个额外的解码器LSTM层,以同时优化并预测未来的序列。这种结构如图5.2所示。需要注意的是,使用原始的平铺图像像素作为输入,也可以将这种架构适配到视频图像模态中。这种架构也被称为seq2seq。
编码器LSTM的隐藏状态输出可以被视为LSTM自编码器的潜在特征。Transformers,一种新的可以处理序列数据的架构,也有一些变体可以被视为一种自编码器——一些transformers可以是自编码器,但并非所有transformers都是自编码器。
对于图像数据,通过使用卷积层,我们可以通过多个卷积和池化层逐步缩小特征,直到应用全局池化层并将数据变为一维特征为止。这代表了自编码器的编码器生成瓶颈特征的过程。这一工作流程与我们在前一节讨论的CNN相同。然而,对于解码器来说,为了将一维池化特征重新扩展为二维的图像样式数据,需要一种特殊形式的卷积,称为转置卷积。
本节提到的变体都是基于标准自编码器结构,但使用不同的神经网络类型来实现它们。还有两个额外的自编码器变体,其中一个基于数据输入的操作,另一个则基于对自编码器结构的实际修改,以实现数据生成目标。
对于用于数据操作的变体,思想是在训练期间向输入数据添加噪声,并保持没有添加噪声的原始输入数据,作为目标用于预测性地重构数据。这个自编码器的变体称为去噪自编码器,因为其主要目标是去除数据中的噪声。由于目标从压缩数据转变为去噪数据,因此瓶颈特征不再局限于较小的尺寸。之后使用的特征不仅限于单一的瓶颈特征,而可以是网络中多个中间层的特征,或者只是简单的去噪重构输出。这种方法利用了神经网络执行自动特征工程的固有能力。去噪自编码器的最著名应用是在由葡萄牙保险公司Porto Seguro主办的基于表格数据的Kaggle比赛中获得第一名的解决方案,其中多个中间层的特征被输入到一个独立的MLP中,预测驾驶员是否将在未来提交保险索赔(www.kaggle.com/competition…)。
对于修改自编码器结构的变体,思想是生成两个瓶颈特征向量,分别表示标准差值和均值,以便基于均值和标准差值对不同的瓶颈特征值进行采样。这些采样得到的瓶颈特征值可以传递给解码器,生成新的随机数据输出。这个自编码器的变体称为变分自编码器。
现在我们已经概述了自编码器变体的内容,接下来让我们深入研究CNN自编码器,并使用深度学习库构建一个CNN自编码器。
构建CNN自编码器
让我们从了解什么是转置卷积开始。图5.3展示了一个示例转置卷积操作,输入大小为2x2,卷积滤波器大小为2x2,步幅为1。
在图5.3中,请注意每个2x2输入数据都标有1到4的数字。这些数字用于映射输出结果,这些输出表现为3x3的输出。卷积核以滑动窗口的方式将每个权重单独应用于输入数据中的每个值,四个卷积操作的输出结果展示在图的底部。在操作完成后,每个输出将按元素相加,形成最终输出,并施加偏置。这个示例过程展示了如何在不完全依赖填充的情况下,将2x2的输入数据扩展为3x3的尺寸。
接下来,我们将在Pytorch中实现一个卷积自编码器模型,并在Fashion MNIST图像数据集上进行训练。这个图像数据集包含了鞋子、包包、衣服等时尚物品。
首先,我们导入必要的库:
import torch.nn as nn
import torchvision
from PIL import Image
接下来,我们将定义整个卷积自编码器的结构:
class ConvAutoencoder(nn.Module):
def __init__(self):
super(ConvAutoencoder, self).__init__()
self.encoder = None
self.decoder = None
def forward(self, x):
bottleneck_feature = self.encoder(x)
reconstructed_x = self.decoder(
bottleneck_feature
)
return reconstructed_x
这里展示的代码是一个PyTorch中的卷积自编码器结构,包含了编码器和解码器的占位符。编码器的职责是接受一张图像并逐渐降低其维度,直到只剩下一个小的表示特征——瓶颈特征。然后,解码器将接受这个瓶颈特征并生成一个与原始输入图像大小相同的特征图。编码器和解码器将在接下来的步骤中定义。
编码器将设计为接受一张灰度图像(单通道),尺寸为28x28。这是Fashion MNIST图像数据集的默认尺寸。以下逻辑展示了如何定义编码器,替换第2步中定义的占位符:
self.encoder = nn.Sequential(
nn.Conv2d(1, 16, 4),
nn.ReLU(),
nn.MaxPool2d(2, 2),
nn.Conv2d(16, 4, 4),
nn.ReLU(),
nn.AvgPool2d(9),
)
定义的编码器包含两个卷积层,每个卷积层后面跟着非线性激活函数ReLU和池化层。卷积层的滤波器大小为16和4。第二个池化层是一个全局平均池化层,旨在将4x9x9的特征图减少到4x1x1,其中每个通道只有一个值来表示自身。这意味着编码器会压缩原始28x28图像的维度,压缩率达到了99.4%!
接下来,解码器将接受这些每张图像的四个特征,并再次生成28x28的输出特征图,重建原始图像尺寸。整个模型没有应用填充。解码器将按如下方式定义,替换第2步中定义的占位符:
self.decoder = nn.Sequential(
nn.ConvTranspose2d(4, 16, 5, stride=2),
nn.ReLU(),
nn.ConvTranspose2d(16, 4, 5, stride=2),
nn.ReLU(),
nn.ConvTranspose2d(4, 1, 4, stride=2),
nn.Sigmoid(),
)
这里使用了三个卷积转置层。每个卷积层后都跟随一个非线性激活层,其中前两层使用了ReLU(标准的非线性激活函数),最后一层使用了sigmoid。使用sigmoid是因为Fashion MNIST数据已经被规范化,值范围在0到1之间。定义的卷积转置层采用了与编码器相似数量的滤波器配置,从16到4,最后到1,生成一个单通道的灰度图像。
现在,我们已经定义了卷积自编码器,接下来让我们从torchvision库加载Fashion MNIST数据集。此教程将使用Catalyst库来简化训练,因此我们将从torchvision获取Fashion MNIST数据加载器并将其修改为Catalyst库的格式:
class FashionMNISTImageTarget(
torchvision.datasets.FashionMNIST
):
def __getitem__(self, index):
img = self.data[index]
img = Image.fromarray(
img.numpy(), mode="L"
)
if self.transform is not None:
img = self.transform(img)
return img, img
从torchvision库中的类已经包含了下载和加载Fashion MNIST数据集的必要逻辑。然而,数据加载方法getitem并不符合图像生成所需的格式,因此需要进行修改,以便这个实验能够正常运行。
注意,第5步使用了Pillow库来加载图像。这是为了方便使用torchvision的工具执行不同的变换步骤,比如图像增强。然而,在这个实验中,我们将直接将Pillow图像转换为PyTorch张量,使用接下来的变换逻辑:
def transform_image(image):
return torchvision.transforms.ToTensor()(image)
现在,让我们加载Fashion MNIST的训练和验证数据集:
train_fashion_mnist_data = FashionMNISTImageTarget(
'fashion_mnist/', download=True, train=True,
transform=transform_image,
)
valid_fashion_mnist_data = FashionMNISTImageTarget(
'fashion_mnist/', download=True, train=False,
transform=transform_image,
)
loaders = {
"train": DataLoader(
train_fashion_mnist_data, batch_size=32,
shuffle=True
),
"valid": DataLoader(
valid_fashion_mnist_data, batch_size=32
),
}
前面的代码会将数据集下载到fashion_mnist文件夹中(如果该文件夹不存在的话)。此外,loaders变量用于被Catalyst库消费。
由于优化目标是减少重建像素值与目标像素值之间的差异,我们将使用均方误差作为重建损失:
criterion = nn.MSELoss()
需要注意的是,尽管重建损失在无监督表示学习中是常见的目标,但根据具体的算法或方法,可能会使用其他度量或目标。例如,在变分自编码器(VAEs)中,目标是最大化证据下界(ELBO),它由重建损失和KL散度组成,鼓励学习到的潜在空间遵循特定的概率分布。另一个例子是感知损失,它可以作为自编码器的损失函数,用于保留高级语义特征,而不是实现逐像素的准确性。
现在,让我们定义Catalyst的监督训练器实例,以便训练我们的模型:
runner = dl.SupervisedRunner(
input_key="features", output_key="scores", target_key="targets", loss_key="loss"
)
接下来,我们将定义一个通用函数,使得通过代码执行多个训练和验证实验变得更加简便:
def train_and_evaluate_mlp(
trial_number, net, epochs,
load_on_stage_start=False, best_or_last='last',
verbose=False
):
model = net
optimizer = optim.Adam(
model.parameters(), lr=0.02
)
checkpoint_logdir = "logs/trial_{}_autoencoder".format(
trial_number)
runner.train(
model=model,
criterion=criterion,
optimizer=optimizer,
loaders=loaders,
num_epochs=epochs,
callbacks=[
dl.CheckpointCallback(
logdir=checkpoint_logdir,
loader_key="valid",
metric_key="loss",
load_on_stage_end='best',
)
],
logdir="./logs",
valid_loader="valid",
valid_metric="loss",
minimize_valid_metric=True,
verbose=verbose,
)
with open(
os.path.join(checkpoint_logdir, '_metrics.json'),
'r'
) as f:
metrics = json.load(f)
if best_or_last == 'last':
valid_loss = metrics['last']['_score_']
else:
valid_loss = metrics['best']['valid']['loss']
return valid_loss
这只是基本的训练和评估模板代码,没有应用任何技巧。我们将在第8章《探索监督深度学习》中深入探讨训练监督模型的技巧。
现在,我们已经准备好通过以下逻辑训练和评估CNN自编码器模型:
cnn_autoencoder = ConvAutoencoder()
best_valid_loss = train_and_evaluate_mlp(
0, cnn_autoencoder, 20, load_on_stage_start=False, best_or_last='last', verbose=True
)
在训练20个epoch后,基于验证损失的最佳表现,cnn_autoencoder的最佳权重将被自动加载。
在提供的训练代码上训练上述模型后,在Fashion MNIST数据集上使用1x28x28的图像维度,您应该得到类似图5.4中示例输入/输出对的结果,可以通过以下代码生成:
input_image = valid_fashion_mnist_data[0][0].numpy()
predicted_image = cnn_autoencoder(
torch.unsqueeze(valid_fashion_mnist_data[0][0], 0)
)
predicted_image = predicted_image.detach().numpy(
).squeeze(0).squeeze(0)
f, axarr = plt.subplots(2,1, figsize=(5, 5))
axarr[0].imshow(predicted_image, cmap='gray')
axarr[1].imshow(input_image.squeeze(0), cmap='gray')
从结果来看,显然瓶颈特征能够在一定程度上重建整个图像,它们可以作为比原始输入数据更具代表性和紧凑性的特征来使用。将瓶颈特征的表示值数量从4增加到10个左右,也应当能够提高可重建图像的质量。可以尝试一下,实验这些参数吧!
总结
自编码器被认为是实现跨数据模态表示学习的基本方法。可以将其架构视为一个框架,您可以将各种其他神经网络组件嵌入其中,从而使其能够处理不同模态的数据,或者从更先进的神经网络组件中获益。
然而,值得注意的是,自编码器并不是学习表示特征的唯一方法。自编码器还有许多应用,主要围绕着使用相同架构的不同训练目标展开。本章简要介绍的两个变种是去噪自编码器和变分自编码器,它们将在第9章《探索无监督深度学习》中得到详细介绍。现在,让我们再次换个话题,来探索一下变换器(transformers)模型家族!