内容总结自花书《Deep Learning》以及《Python 深度学习》。
自编码器(autoencoder)是神经网络的一种,经过训练后能尝试将输入复制到输出。自编码器内部有一个隐藏层 h h h,可以产生编码来表示输入。
我们可以将自编码器看作由两部分组成:一个由函数 h = f ( x ) h=f(x) h=f(x) 表示的编码器和一个生成重构的解码器 r = g ( h ) r=g(h) r=g(h)。
但如果只是简单的复制输入,自编码器就没有什么特别的用处。我们通常会向自编码器强加一些约束,使它只能近似的复制,并只能复制与训练数据相似的输入。这些约束强制模型考虑输入数据的哪些部分需要被优先复制,因此往往能学到数据的有用特性。
传统自编码器被用于降维或特征学习,近年来,自编码器与潜变量模型理论的联系将自编码器带到了生成式建模的前沿。图像生成的关键思想就是找到一个低维的表示潜在空间(latent space),其中任意点都可以被映射为一张逼真的图像。一旦找到了这样的潜在空间,就可以从中随机采样,并将其映射到图像空间,从而生成前所未见的图像:
想要学习图像表示的这种潜在空间,GAN 和 VAE(变分自编码器)是两种不同的策略。VAE 非常适合用于学习具有良好结构的潜在空间,其中特定方向表示数据中有意义的变化轴,GAN 生成的图像可能非常逼真,但它的潜在空间可能没有良好结构,也没有足够的连续性。我们稍后就会介绍 VAE。
传统的图像自编码器接收一张图像,通过一个编码器模块将其映射到潜在向量空间,然后再通过一个解码器模块将其解码为与原始图像具有相同尺寸的输出。然后,使用与输入图像相同的图像作为目标数据来训练这个自编码器。
我们刚刚也提到过,将输入复制到输出听起来没什么用,但我们通常不关心解码器的输出,相反,我们希望通过训练自编码器对输入进行复制而使 h h h 获得有用的特性。
从自编码器获得有用特征的一种方法是限制 h h h 的维度比 x x x 小,这种编码维度小于输入维度的自编码器称为欠完备(undercomplete)自编码器。学习欠完备的表示将强制自编码器捕捉训练数据中最显著的特征。
但如果编码器和解码器被赋予过大的容量,自编码器会执行复制任务而不会捕捉到数据的有用特征。
编码维数小于输入维数的欠完备自编码器可以学习数据分布最显著的特征,但如果赋予这类自编码器过大的容量,它就不能学到任何有用的信息。我们可以通过对编码(即编码器的输出)施加各种限制,让自编码器学到比较有趣的数据潜在表示。
稀疏自编码器简单的在训练时结合编码层的系数惩罚 Ω ( h ) \Omega(h) Ω(h) 和重构误差:
L ( x , g ( f ( x ) ) ) + Ω ( h ) L(x,g(f(x)))+\Omega(h) L(x,g(f(x)))+Ω(h)
参数的概念和我们上面提到的一致,即 h = f ( x ) h=f(x) h=f(x) 表示编码器的输出。稀疏自编码器一般用来学习特征,以便用于像分类这样的任务。
传统的自编码器最小化重构误差
L ( x , g ( f ( x ) ) ) L(x,g(f(x))) L(x,g(f(x)))
而去噪自编码器(denoising autoencoder, DAE)则最小化
L ( x , g ( f ( x ~ ) ) ) L(x,g(f(\tilde{x}))) L(x,g(f(x~)))
其中 x ~ \tilde{x} x~ 是被某种噪声损坏的 x x x 的副本。因此去噪自编码器必须撤销这些损坏,而不是简单的复制输入。我们会引入一个损坏过程 C ( x ~ ∣ x ) C(\tilde{x}|x) C(x~∣x),这个条件分布代表给定数据样本 x x x 产生损坏样本 x ~ \tilde{x} x~ 的概率。自编码器根据以下过程,从训练数据对 ( x , x ~ ) (x,\tilde{x}) (x,x~) 中学习重构分布 p r e c o n s t r u c t ( x ∣ x ~ ) p_{reconstruct}(x|\tilde{x}) preconstruct(x∣x~):
我们可以将去噪自编码器理解为将损坏的数据点 x ~ \tilde{x} x~ 映射回原始数据点 x x x:
上图中,虚线圆圈就代表损坏过程 C ( x ~ ∣ x ) C(\tilde{x}|x) C(x~∣x),自编码器学习的就是紫色虚线箭头所表示的向量场 g ( f ( x ) ) − x g(f(x))-x g(f(x))−x.
另一正则化自编码器的策略是使用一个类似稀疏自编码器中的惩罚项 Ω \Omega Ω:
L ( x , g ( f ( x ) ) ) + Ω ( h , x ) L(x,g(f(x)))+\Omega(h,x) L(x,g(f(x)))+Ω(h,x)
其中
Ω ( h , x ) = λ ∑ i ∥ ∇ x h i ∥ 2 \Omega(h,x)=\lambda\sum_i \Vert\nabla_xh_i\Vert^2 Ω(h,x)=λi∑∥∇xhi∥2
这迫使模型学习一个在 x x x 变化小时目标也没有太大变化的函数。这样正则化的自编码器称为收缩自编码器(contractive autoencoder, CAE)。
VAE 不是将输入图像压缩成潜在空间中的固定编码,而是将图像转换为统计分布的参数,即平均值和方差。本质上来说,这意味着我们假设输入图像是由统计过程生成的,在编码和解码过程中应该考虑这一过程的随机性。然后,VAE 使用平均值和方差这两个参数来从分布中随机采样一个元素,并将这个元素解码到原始输入。
从技术角度来说,VAE 的工作原理如下:
input_img
转换为表示潜在空间中的两个参数 z_mean
和 z_log_variance
;z_mean + exp(z_log_variance) * epsilon
,其中 epsilon
是取值很小的随机张量因为 epsilon
是随机的,所以这个过程可以确保,与 input_img
编码的潜在位置靠近的每个点都能被解码为与 input_img
类似的图像,从而迫使潜在空间能够连续的有意义。潜在空间中任意两个相邻的点都会被解码为高度相似的图像。连续性以及潜在空间的低维度,将迫使潜在空间中的每个方向都表示数据中一个有意义的变化轴,这使得潜在空间具有非常良好的结构。
VAE 的大致代码如下:
z_mean, z_log_variance = encoder(input_img) # 将输入编码为均值和方差两个参数
z = z_mean + exp(z_log_var) * epsilon
reconstructed_img = decoder(z) # 将 z 解码为一张图像
model = Model(input_img, reconstructed_img) # 实例化自编码器模型
下面我们先定义编码器网络:
import keras
from keras import layers
from keras import backend as K
from keras.models import Model
import numpy as np
img_shape = (28, 28, 1)
batch_size = 16
latent_dim = 2 # 潜在空间维度:一个二维平面
input_img = keras.Input(shape=img_shape)
x = layers.Conv2D(32, 3, padding='same', activation='relu')(input_img)
x = layers.Conv2D(64, 3, padding='same', activation='relu',
strides=(2, 2))(x)
x = layers.Conv2D(64, 3, padding='same', activation='relu')(x)
x = layers.Conv2D(64, 3, padding='same', activation='relu')(x)
shape_before_flattening = K.int_shape(x)
x = layers.Flatten()(x)
x = layers.Dense(32, activation='relu')(x)
z_mean = layers.Dense(latent_dim)(x)
z_log_var = layers.Dense(latent_dim)(x)
接下来的代码将使用 z_mean
和 z_log_var
来生成一个潜在空间点 z z z。在 keras 中,任何对象都应该是一个层,所以如果代码不是内置层的一部分,我们应该将其包装到一个 Lambda
层(或自定义层)中。
def sampling(args):
z_mean, z_log_var = args
epsilon = K.random_normal(shape=(K.shape(z_mean)[0], latent_dim),
mean=0., stddev=1.)
return z_mean + K.exp(z_log_var) * epsilon
z = layers.Lambda(sampling)([z_mean, z_log_var])
下列代码给出了解码器的实现:
decoder_input = layers.Input(K.int_shape(z)[1:])
x = layers.Dense(np.prod(shape_before_flattening[1:]),
activation='relu')(decoder_input)
x = layers.Reshape(shape_before_flattening[1:])(x)
x = layers.Conv2DTranspose(32, 3, padding='same', activation='relu',
strides=(2, 2))(x)
x = layers.Conv2D(1, 3, padding='same', activation='sigmoid')(x)
decoder = Model(decoder_input, x)
z_decoded = decoder(z)
VAE 有重构损失以及正则化损失的双重损失,我们需要编写一个自定义层,并在其内部使用内置的 add_loss
层方法来创建我们想要的损失:
class CustomVariationalLayer(keras.layers.Layer):
def vae_loss(self, x, z_decoded):
x = K.flatten(x)
z_decoded = K.flatten(z_decoded)
xent_loss = keras.metrics.binary_crossentropy(x, z_decoded)
kl_loss = -5e-4 * K.mean(1 + z_log_var - K.square(z_mean) - K.exp(z_log_var), axis=-1)
return K.mean(xent_loss + kl_loss)
def call(self, inputs):
x = inputs[0]
z_decoded = inputs[1]
loss = self.vae_loss(x, z_decoded)
self.add_loss(loss, inputs=inputs)
return x
y = CustomVariationalLayer()([input_img, z_decoded])
最后,将模型实例化并开始训练。因为损失包含在自定义层中,所以在编译时无须指定外部损失(即 loss=None
),这意味着在训练过程中不需要传入目标数据。
from keras.datasets import mnist
vae = Model(input_img, y)
vae.compile(optimizer='rmsprop', loss=None)
vae.summary()
(x_train, _), (x_test, y_test) = mnist.load_data()
x_train = x_train.astype('float32') / 255.
x_train = x_train.reshape(x_train.shape + (1,))
x_test = x_test.astype('float32') / 255.
x_test = x_test.reshape(x_test.shape + (1,))
vae.fit(x=x_train, y=None,
shuffle=True,
epochs=10,
batch_size=batch_size,
validation_data=(x_test, None))
如果到这一步发现报错,可以在开头加入下面的语句,并重新运行上述所有代码:
from tensorflow.python.framework.ops import disable_eager_execution
disable_eager_execution()
模型训练好后,我们就可以使用 decoder
网络将任意潜在空间向量转换为图像:
import matplotlib.pyplot as plt
from scipy.stats import norm
n = 15
digit_size = 28
figure = np.zeros((digit_size * n, digit_size * n))
grid_x = norm.ppf(np.linspace(0.05, 0.95, n))
grid_y = norm.ppf(np.linspace(0.05, 0.95, n))
for i, yi in enumerate(grid_x):
for j, xi in enumerate(grid_y):
z_sample = np.array([[xi, yi]])
z_sample = np.tile(z_sample, batch_size).reshape(batch_size, 2)
x_decoded = decoder.predict(z_sample, batch_size=batch_size)
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))
plt.imshow(figure, cmap='Greys_r')
plt.show()
当我们沿着潜在空间的一条路径观察时,会观察到一个数字逐渐变形为另一个数字。
[1] 《Python 深度学习》,François Chollet.
[2] I. J. Goodfellow, Y. Bengio, and A. Courville, Deep Learning. Cambridge, MA, USA: MIT Press, 2016, http://www.deeplearningbook.org.