第 18 章 自动编码器

在前面的章节中,我们介绍了各种类型的无监督学习算法,如聚类算法、降维算法等。归根结底,无监督学习的目的是从复杂数据中提取出可以代表数据的特征,例如数据的分布、数据的主要成分等等,再用这些信息帮助后续的其他任务。随着机器学习向深度学习发展和神经网络的广泛应用,用神经网络提取数据特征的方法也越来越重要。本章介绍的自动编码器(autoencoder,AE)就是其中最基础的一种无监督特征提取方法。

自动编码器的原理并不复杂,它将一个输入数据样本压缩成一个低维特征向量表示,然后试图基于该低维特征向量恢复出原数据样本。可以想象,如果该低维特征向量能充分保留原数据样本的信息,那么就可能基于该低维特征向量较好地恢复出原数据。比如在图18-1中,我们希望把梵高的《星空》存储在一台机器中,但是这幅画的细节非常丰富,如果用非常高的精度存储,要占用很大的空间。因此,我们可以通过某种算法,把这幅画编码成较少的数据;需要读取时,再通过对应的算法解码出来。这样,虽然解码出的画丢失了一些细节,但是存储的开销也大大降低了。计算机中常见的图像格式JPEG就是一种有损的图像编码方法。自动编码器也一样,当我们提取特征时,必然也会保留主要特征、丢弃次要特征,因此最后解码的结果通常不会和输入完全相同。

encode_decode
图 18-1 编码与解码

设数据集,其中每个样本。在PCA一章中,我们通过矩阵分解提取出了使样本方差最大的个主成分。然而,当样本数量或者样本维度较大时,PCA的计算复杂度非常高。并且如果样本的有效特征并非样本当前维度的线性组合,而是要经过非线性变换才能得到,那么PCA算法就无能为力了。为了解决这一问题,我们可以用神经网络中的非线性激活函数来引入非线性成分。利用神经网络强大的函数拟合能力,我们就可以近似任意的非线性变换,从而得到质量较高的样本特征。由于将高维样本映射得到的低维特征向量可以看作是样本的编码,提取样本特征的模块称作编码器(encoder)。而基于低维特征向量恢复出接近原始样本的模块称作解码器(decoder)。

编码器和解码器的设计方式有很多。如果我们对数据分布有足够的先验知识,当然可以直接通过这些知识来对数据做编码和解码。例如,如果所有的样本都是独热向量,我们就可以用的正整数来编码样本,表示值为的维度的下标,这样解码也很直接。但是,多数时候样本的分布非常复杂,我们很难用简单的分析手段就得出其分布情况。因此,我们可以用神经网络来直接学习编码器和解码器,并用反向传播等方式自动更新其参数,这就是自动编码器。下面,我们来具体讲解自动编码器的结构和训练方式。

18.1 自动编码器的结构

设编码器表示的映射为,将样本变换为特征向量。以最简单的单层感知机为例,其变换由一次线性变换和一次非线性的激活函数复合而成:

其中是网络参数,是激活函数。我们知道,在监督学习中神经网络参数的更新需要有监督信号、即样本的标签,用神经网络的预测和真实的样本标签计算出损失,再用损失的梯度回传更新参数。然而在无监督学习中,我们无法获得监督信号,并且由于我们缺乏对数据分布的认知,很难评判训练得到的特征的质量、得到训练损失,也就无法更新网络参数。

这时,我们可以来考虑编码器的任务目标。编码器需要将高维的样本变换为低维的特征,并且这些特征应当保留原始样本尽可能多的信息。从高维到低维的变换中必定伴随着不可逆的信息损失,如果特征质量较差,保留的信息较少,那么我们无论如何都不可能从特征恢复出原始样本。反过来说,我们可以引入第二个网络,将特征再变回接近原始样本的输出

其中是网络参数,是激活函数。如果该网络可以尽可能将特征恢复成原始样本,就说明我们得到的特征质量较高。因此,我们就可以将恢复出的样本与原始样本之间的差别作为特征的评价指标。假设损失函数是MSE,那么总的损失可以写为:

该损失又称为重建损失(reconstruction loss)。由于将编码映射回原空间,与编码器的作用相反,我们将其称为解码器(decoder)。从上式中可以看出,无论编码器与解码器的形式如何,我们都可以用重建损失的梯度来更新网络参数,与监督学习的方式很相似。像这样在无监督学习任务中,从数据集中自行构造出监督信号进行学习的方法就称为自监督学习(self-supervised learning)。需要注意,自监督学习中用到的监督信号也来自于样本自身,并非引入了额外的信息,因此它仍然属于无监督学习的范畴。

将上面的编码器和解码器组合起来,就得到了自动编码器,其结构如图18-2所示。通常来说,自动编码器的结构不会特别复杂,简单的MLP就足够满足任务的要求。考虑到编码与解码过程的对称性,设编码器的隐藏层大小依次为 ,也就是说权重矩阵的维度为 ,我们一般会将解码器的隐层大小依次设置为 ,与编码器相反。但是由于非线性激活函数的存在,编码与解码过程并不完全对称,其权重应当不同,且解码器的权重与编码器的权重甚至大概率没有关联。

autoencoder
图 18-2 自动编码器的结构示意

下面,我们在手写数字数据集MNIST上实现自动编码器,用自动编码器提取图像的特征,并观察用解码器还原后的效果。

18.2 动手实现自动编码器

在K近邻算法一章中,我们已经介绍过MNIST数据集的内容。该数据集包含一些手写数字的黑白图像,其中白色的部分是数字,黑色的部分是背景,所有图像的大小都是2828,且只有黑白两种颜色。由于图像大小较大,占用存储空间,并且通常还有许多空间上的关联信息。如果我们要完成基于图像上的任务,既可以利用卷积神经网络来提取其空间特征,也可以先从图像中提取出一些一维的特征,再用更简单的网络结构进行训练,降低训练的复杂度。因此,我们希望用自动编码器完成这一任务。首先,我们导入必要的库和数据集。

import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import torch
import torch.nn as nn
# 导入数据
mnist_train = pd.read_csv("mnist_train.csv")
mnist_test = pd.read_csv("mnist_test.csv")
# 提取出图像信息,并将内容从0~255的整数转换为0.0~1.0的浮点数
# 图像大小为28*28,数组中每一行代表一张图像
x_train = mnist_train.iloc[:, 1:].to_numpy().reshape(-1, 28 * 28) / 255
x_test = mnist_test.iloc[:, 1:].to_numpy().reshape(-1, 28 * 28) / 255
print(f'训练集大小:{len(x_train)}')
print(f'测试集大小:{len(x_test)}')
训练集大小:60000
测试集大小:10000

我们先来展示部分数据集中的图像,来对数据集有更清晰的认识。考虑到后面还要比较重建图像和原始图像,我们把展示图像的方法写成函数。

def display(data, m, n):
# data:图像的像素数据,每行代表一张图像
# m,n:按m行n列的方式展示前m * n张图像
img = np.zeros((28 * m, 28 * n))
for i in range(m):
for j in range(n):
# 填充第i行j列图像的数据
img[i * 28: (i + 1) * 28, j * 28: (j + 1) * 28] = \
data[i * m + j].reshape(28, 28)
plt.figure(figsize=(m * 1.5, n * 1.5))
plt.imshow(img, cmap='gray')
plt.show()
display(x_test, 3, 5)


png

接下来,我们来用PyTorch库实现自动编码器的网络结构。这里,我们用两层隐层的MLP作为编码器和解码器,且全部使用逻辑斯谛激活函数。由于两者结构本质上相同,我们只实现一个MLP类,再分别实例化为编码器和解码器。原始图像拉平成一维后大小是,之后的隐层大小我们选择256和128,最后输出的特征向量大小为100。这些参数的选择都只是默认的,读者可以自行调整隐层和特征向量的大小,观察编码效果的变化。

# 多层感知机
class MLP(nn.Module):
def __init__(self, layer_sizes):
super().__init__()
self.layers = nn.ModuleList() # ModuleList用列表存储PyTorch模块
num_in = layer_sizes[0]
for num_out in layer_sizes[1:]:
# 创建全连接层
self.layers.append(nn.Linear(num_in, num_out))
# 创建逻辑斯谛激活函数层
self.layers.append(nn.Sigmoid())
num_in = num_out
def forward(self, x):
# 前向传播
for l in self.layers:
x = l(x)
return x
layer_sizes = [784, 256, 128, 100]
encoder = MLP(layer_sizes)
decoder = MLP(layer_sizes[::-1]) # 解码器的各层大小与编码器相反

我们按照上面讲解的方式,先用编码器计算出每个样本的编码,再用解码器计算恢复出的样本,计算之间的重建损失,通过重建损失来训练编码器和解码器的参数。训练过程我们利用PyTorch进行自动化,并采用Adam优化器。下面,我们设置训练所需的超参数。在训练过程中,为了更清晰地展示编码质量的变化,我们每隔一定轮数就将重建的图像绘制出来,展示其随训练过程的变化。

# 训练超参数
learning_rate = 0.01 # 学习率
max_epoch = 10 # 训练轮数
batch_size = 256 # 批量大小
display_step = 2 # 展示间隔
np.random.seed(0)
torch.manual_seed(0)
# 采用Adam优化器,编码器和解码器的参数共同优化
optimizer = torch.optim.Adam(list(encoder.parameters()) \
+ list(decoder.parameters()), lr=learning_rate)
# 开始训练
for i in range(max_epoch):
# 打乱训练样本
idx = np.arange(len(x_train))
idx = np.random.permutation(idx)
x_train = x_train[idx]
st = 0
ave_loss = [] # 记录每一轮的平均损失
while st < len(x_train):
# 遍历数据集
ed = min(st + batch_size, len(x_train))
X = torch.from_numpy(x_train[st: ed]).to(torch.float32)
Z = encoder(X)
X_rec = decoder(Z)
loss = 0.5 * nn.functional.mse_loss(X, X_rec) # 重建损失
ave_loss.append(loss.item())
optimizer.zero_grad()
loss.backward() # 梯度反向传播
optimizer.step()
st = ed
ave_loss = np.average(ave_loss)
if i % display_step == 0 or i == max_epoch - 1:
print(f'训练轮数:{i},平均损失:{ave_loss:.4f}')
# 选取测试集中的部分图像重建并展示
with torch.inference_mode():
X_test = torch.from_numpy(x_test[:3 * 5]).to(torch.float32)
X_test_rec = decoder(encoder(X_test))
X_test_rec = X_test_rec.cpu().numpy()
display(X_test_rec, 3, 5)
训练轮数:0,平均损失:0.0307

png

训练轮数:2,平均损失:0.0166

png

训练轮数:4,平均损失:0.0126

png

训练轮数:6,平均损失:0.0106

png

训练轮数:8,平均损失:0.0096

png

训练轮数:9,平均损失:0.0089

png

最后,我们把得到的模型在测试集上选取部分图像进行重建,并与原图比较,观察模型的效果。可以看出,重建的图像与原始图像非常相近,肉眼很容易辨认出重建图像中的数字,但也能观察出部分缺失的细节。然而,原始图像的大小是784像素,而经由编码器得到的编码长度只有100,大大减小了数据的复杂度。即使算上解码器的模型参数,因为解码器对所有图像的编码都是通用的,无非是加上一个常数。但是需要存储的图像越多,由编码节约的空间就越大,完全可以覆盖模型参数需要的空间了。

print('原始图像')
display(x_test, 3, 5)
print('重建图像')
X_test = torch.from_numpy(x_test[:3 * 5]).to(torch.float32)
X_test_rec = decoder(encoder(X_test))
X_test_rec = X_test_rec.detach().cpu().numpy()
display(X_test_rec, 3, 5)
原始图片

png

重建图片

png

18.3 本章小结

本章介绍了无监督学习和深度学习中的重要模型之一——自动编码器。它结构简单,不依赖监督信号,只需要数据本身,易于和其他模块结合,可以作为复杂任务的数据处理和特征提取步骤。例如我们要完成手写数字分类任务,就可以先用自动编码器获得样本的特征,再用这些特征作为输入,训练其他有监督学习任务的机器学习模型。自动编码器的这种自监督学习范式是现代深度学习中的一种非常重要的范式,也是机器学习里重要的思维方式之一。

除了上面讲解的最简单的自动编码器之外,它还有许多变式。栈式自动编码器(stacked autoencoder)采用分层训练的方式,先训练只有一层的MLP编码器和解码器。第一层训练完成后,再固定其参数,添加第二层,用同样的方法训练第二层的参数,依次类推。这种方式减小了训练多层复杂编码器的难度,但也会增加训练时间。降噪自动编码器(denoising autoencoder)通过对输入数据样本加噪,再通过自动编码器恢复原始数据样本的方式,让模型能对带有噪音的数据样本做降噪和编码。将自动编码器和贝叶斯推断结合可以得到变分自动编码器(variational autoencoder,VAE),其中的编码器和解码器分别拟合特征的后验分布和样本的条件分布。在VAE训练完毕后,可以通过在编码空间中采样不同的,用解码器生成与真实样本相似的虚拟样本。因此,VAE常被视为生成式模型,用来拟合数据分布,生成同一分布的更多数据用于后续训练。在如今的计算机视觉和自然语言处理领域中,由于输入的图像或文本维度都相当大,编码器已经成为了模型中必不可少的部分,而编码器结构的设计也是算法十分重要的一个环节,有着大量而广泛的应用。

习题

  1. 以下关于自动编码器的说法不正确的是: A. 自动编码器是一种特征提取技术,还可以用来去噪。 B. 自动编码器的训练方式属于无监督学习。 C. 自动编码器得到的编码完整保留了原始输入的信息,从而可以再用解码器还原。 D. 自动编码器的编码部分和解码部分是一体的,无法分开训练。

  2. 自动编码器作为特征提取结构,可以和其他算法组合。将本章的自动编码器提取出的特征输入到MLP里,利用MLP完成有监督的手写数字分类任务。

  3. 自动编码器的基础结构并不一定局限于MLP,对于图像任务来说,CNN在理论上更加合适。尝试用CNN搭建自动编码器,该模型的解码部分同样与编码部分结构相同、顺序相反,并且将编码时的池化用上采样代替。

  4. 降噪编码器是自动编码器的一个变种,它主动为输入样本添加噪声,将带噪的样本给自动编码器训练,与原始样本计算重建损失。这样训练出的自动编码器就有了去噪功能。试给手写数字图像加上噪声,用降噪编码器为其去噪,观察去噪后的图像与原始图像的区别。

参考文献

[1] 栈式自动编码器论文:Bengio Y, Lamblin P, Popovici D, et al. Greedy layer-wise training of deep networks[J]. Advances in neural information processing systems, 2006, 19.

[2] 降噪自动编码器论文:Vincent P, Larochelle H, Bengio Y, et al. Extracting and composing robust features with denoising autoencoders[C]//Proceedings of the 25th international conference on Machine learning. 2008: 1096-1103.

[3] 变分自动编码器论文:Kingma D P, Welling M. Auto-encoding variational bayes[J]. arXiv preprint arXiv:1312.6114, 2013.