0. 前言
我们已经学习了计算机如何将图像视为像素,并设计了概率模型来分配像素以生成图像。但是,这不是生成图像的最有效方法。我们先逐个查看图像,然后尝试了解其中的内容,而不是逐个像素地扫描图像。例如,一个女孩正坐着,戴着帽子,微笑着。然后,我们使用该信息绘制人像。这就是自编码器的工作方式。在本节中,我们将学习如何使用自编码器将像素编码为可以从中采样以生成图像的潜变量。
1. 使用自编码器学习潜变量
自编码器于 1980
年代 Geoffrey Hinton
等人首次推出。高维输入空间中有很多冗余,可以压缩成一些低维变量。有传统的机器学习技术用于减少输入维度,例如主成分分析 (Principal Component Analysis
, PCA)
。
但是,在图像生成中,我们还将希望将低维空间还原为高维空间。尽管这样做的方式大不相同,但可以将其视为图像压缩,其中将原始图像压缩为 JPEG
之类的文件格式,该文件格式较小且易于存储和传输。然后,计算机可以将 JPEG
恢复为我们可以看到和操纵的像素。换句话说,原始像素被压缩为低维 JPEG
格式,并恢复为高维原始像素以进行显示。
自编码器是一种无监督的机器学习技术,不需要训练标签就可以对模型进行训练。但是,由于我们确实需要使用标签,因此有人将其称为自监督机器学习( auto
在拉丁语中表示 self
),这些标签是图像本身。
自编码器的基本构建块是编码器和解码器。编码器负责将高维输入减少为一些低维潜(隐)变量。解码器是将隐变量转换回高维空间的模块。编码器-解码器体系结构还用于其他机器学习任务中,例如语义分割,其中神经网络首先了解图像表示,然后生成像素级标签。下图显示了自编码器的一般体系结构:
输入和输出是相同维度的图像,
z
z
z 是低维度的潜矢量。编码器将输入压缩为
z
z
z,解码器将处理反向以生成输出图像。
在检查了整体架构之后,我们学习编码器的工作方式。
2. 编码器
编码器由多个神经网络层组成,本节使用全连接层来说明。现在,我们将直接为 MNIST
数据集构建编码器,该编码器的尺寸为 28x28x1
。我们需要设置潜变量的维数,这里使用一维向量。我们将遵循约定并将潜变量命名为 z
:
def Encoder(z_dim):
inputs = layers.Input(shape=[28,28,1])
x = inputs
x = Flatten()(x)
x = Dense(128, activation='relu')(x)
x = Dense(64, activation='relu')(x)
x = Dense(32, activation='relu')(x)
z = Dense(z_dim, activation='relu')(x)
return Model(inputs=inputs, outputs=z, name='encoder')
潜变量的大小应小于输入尺寸。它是一个超参数,首先尝试使用 10
,它将为我们提供 28 * 28 / 10 = 78.4
的压缩率。
我们将使用三个全连接层,其中神经元的数量逐渐减少( 128
、64
、32
,最后是 10
,这是我们的潜变量 z
的维度),网络输出中的特征尺寸从 784
逐渐减小到 10
。
这种网络拓使模型学习重要的知识,并逐层丢弃次要的特征,最终得到 10
个最重要的特征。它看起来与 CNN 分类非常相似,在 CNN
分类中,特征图的大小自上到下逐渐减小。特征图 (Feature map
) 的大小是指张量的 (height, width)
维度。
由于 CNN
效率更高且更适合图像输入,因此将使用卷积层构建编码器。前期的 CNN
(例如 VGG
) 使用最大池化进行特征图下采样,但是较新的网络倾向于通过在卷积层中使用步幅为 2
的卷积来实现此目的。下图说明了步幅为 2
的卷积核进行滑动以生成特征图,该结果特征图的大小是输入特征图的一半:
在此示例中,我们使用的四个卷积层均具有 8
个卷积核,并将步幅设为 2
以进行下采样:
def Encoder(z_dim):
inputs = layers.Input(shape=[28,28,1])
x = inputs
x = Conv2D(filters=8, kernel_size=(3,3), strides=2, padding='same', activation='relu')(x)
x = Conv2D(filters=8, kernel_size=(3,3), strides=1, padding='same', activation='relu')(x)
x = Conv2D(filters=8, kernel_size=(3,3), strides=2, padding='same', activation='relu')(x)
x = Conv2D(filters=8, kernel_size=(3,3), strides=1, padding='same', activation='relu')(x)
x = Flatten()(x)
out = Dense(z_dim, activation='relu')(x)
return Model(inputs=inputs, outputs=out, name='encoder')
在典型的 CNN
架构中,滤波器的数量增加,而特征图的大小减小。但是,我们的目标是减小尺寸,因此将滤波器的尺寸保持不变,这对于诸如 MNIST
之类的简单数据就足够了。最后,我们将最后一个卷积层的输出展平,并将其馈送到密集层以输出潜变量。
3. 解码器
解码器的工作本质上与编码器相反,其将低维潜变量转换为高维输出以近似输入图像。解码器中的各层无需看起来像编码器的相反顺序。完全可以使用不同的层,例如,仅在编码器中使用全连接层,而仅在解码器中使用卷积层。此处,仍将在解码器中使用卷积层将特征图从 7x7
上采样到 28x28
:
def Decoder(z_dim):
inputs = layers.Input(shape=[z_dim])
x = inputs
x = Dense(7*7*64, activation='relu')(x)
x = Reshape((7,7,64))(x)
x = Conv2D(filters=64, kernel_size=(3,3), strides=1, padding='same', activation='relu')(x)
x = UpSampling2D((2,2))(x)
x = Conv2D(filters=32, kernel_size=(3,3), strides=1, padding='same', activation='relu')(x)
x = UpSampling2D((2,2))(x)
x = Conv2D(filters=32, kernel_size=(3,3), strides=2, padding='same', activation='relu')(x)
out = Conv2(filters=1, kernel_size=(3,3), strides=1, padding='same', activation='sigmoid')(x)
return Model(inputs=inputs, outputs=out, name='decoder')
第一层是全连接层,它接受潜变量并产生一个张量,其大小为我们第一个卷积层的输入尺寸 (7 x 7 x 卷积核数量)
。与编码器不同,解码器的目的不是降低尺寸,因此我们应该使用更多的滤波器来赋予其更强大的生成能力。
UpSampling2D
对像素进行插值以提高分辨率。这是一个仿射变换(线性乘法和加法),因此可以反向传播,但是它使用固定权重,因此是不可训练的。另一种流行的上采样方法是使用转置卷积层 (transpose convolutional layer
),该层是可训练的,但是它可能在生成的图像中创建类似于棋盘方格的伪像。对于低维图像或放大图像时,棋盘状伪像更加明显。通过使用偶数卷积核大小(例如4
)可以减小此效应。因此,图像生成模型倾向于不使用转置卷积。
设计
CNN
时,重要的是要知道如何计算卷积层的输出张量形状。如果使用padding ='same'
,则输出特征图将具有与输入特征相同的大小(高度和宽度)。如果改用padding ='valid'
,则输出大小可能会略小,具体取决于卷积核尺寸。当输入stride = 2
并使用'same'
填充时,特征图的大小将减半。最后,输出张量的通道数与卷积滤波器数相同。例如,如果输入张量的形状为(28,28,1)
并经过conv2d(filters = 32, strides = 2, padding = 'same')
,输出的形状将为(14, 14, 32)
。
4. 构建自编码器
现在,我们将编码器和解码器放在一起以创建自编码器。首先,我们分别实例化编码器和解码器。然后,我们将编码器的输出馈送到解码器的输入中,并使用编码器的输入和解码器的输出实例化一个 Model
:
z_dim = 10
encoder = Encoder(z_dim)
decoder = Decoder(z_dim)
model_input = encoder.input
model_output = decoder(encoder.output)
autoencoder = Model(model_input, model_output)
深度神经网络看起来可能很复杂且难以构建。但是,我们可以将其分解为较小的块或模块,然后将它们放在一起。整个任务变得更易于管理!为了进行训练,我们将使用 L2
损失,这是通过均方差 (mean squared error
, MSE
) 来比较输出和预期结果之间的每个像素而实现的。在此示例中,添加了一些回调函数,它们将在训练每个 epoch
之后进行调用:
ModelCheckpoint(monitor='val_loss')
用于在当前验证损失低于先前epoch
情况下保存模型- 如果验证损失在
10
个epoch
内没有得到改善,则EarlyStopping(monitor='val_loss', patience = 10)
可以更早地停止训练。
生成的图像如下:
第一行是输入图像,第二行是由自编码器生成的。我们可以看到生成的图像有些模糊;这可能是因为我们对其进行了过多压缩,并且在此过程中丢失了一些数据信息。
为了证实我们的猜想,我们可以将潜变量的维数从 10
增加到 100
并生成输出。
5. 利用潜变量生成图像
那么,我们如何使用自编码器?使用 AI
模型将图像转换为自身的模糊版本不是很有用。自编码器的应用之一是图像去噪,即在输入图像中添加一些噪声并训练模型以生成清晰图像。但是,我们对使用它生成图像更感兴趣。因此,让我们看看我们如何做到这一点。
既然我们拥有训练有素的自编码器,可以忽略编码器,而仅使用解码器从潜变量中采样以生成图像。我们面临的第一个挑战是确定如何从潜变量中采样。由于我们在潜变量之前的最后一层没有使用任何激活函数,因此潜空间是无界的,可以是任何实际的浮点数。
为了说明这是如何工作的,我们将使用 z_dim = 2
训练另一个自编码器,以便我们可以在两个维度上探索潜空间。下图显示了潜空间的可视化:
通过将 1000
个样本传递到经过训练的编码器中并在散点图上绘制两个潜变量来生成该图。右侧的颜色栏指示数字标签的强度。我们可以从图中观察到以下内容:
潜变量大约在 –5
到 +4
之间。除非我们创建此图并进行查看,否则我们将不知道确切的范围。再次训练模型时,这种情况可能会改变,并且通常情况下,样本的散布范围可能会超出 ±10
。
这些类别不是均匀分布的。可以在左上方和右上方看到与其他类别完全分开的群集。但是,位于图中心的类趋于更密集地排列,并且彼此重叠。
在以下图像中,可能可以更好地看到不均匀性,这些图像是通过以 1.0
的间隔在潜变量 [-5, +5]
范围生成的:
我们可以看到数字 0
和 1
在样本分布中得到了很好的表示,并且它们也被很好地绘制。中间的数字却是模糊的,甚至样本中也缺少一些数字。
如果仔细看,会发现数字 1
如何变成 7
,然后变成 9
和 4
,看起来自编码器已经了解了潜变量之间的某种关系。