变分自编码器(VAE)
Diederik Kingma 和 Max Welling 于 2014 年推出了另一类重要的自编码器,并迅速成为最受欢迎的自编码器类型之一:变分自编码器。
它们与我们迄今为止讨论的所有自编码器完全不同,特别是:
- 它们是概率自编码器,意味着即使在训练之后,它们的输出部分也是偶然确定的(相对于仅在训练过程中使用随机性的自编码器的去噪)。
- 最重要的是,它们是生成自编码器,这意味着它们可以生成看起来像从训练集中采样的新实例。
这两个属性使它们与 RBM 非常相似(见附录 E),但它们更容易训练,并且取样过程更快(在 RBM 之前,您需要等待网络稳定在“热平衡”之后才能进行取样一个新的实例)
我们来看看他们是如何工作的。 图 15-11(左)显示了一个变分自编码器。 当然,您可以认识到所有自编码器的基本结构,编码器后跟解码器(在本例中,它们都有两个隐藏层),但有一个转折点:不是直接为给定的输入生成编码 ,编码器产生平均编码μ
和标准差σ
。 然后从平均值μ
和标准差σ
的高斯分布随机采样实际编码。 之后,解码器正常解码采样的编码。 该图的右侧部分显示了一个训练实例通过此自编码器。 首先,编码器产生μ
和σ
,随后对编码进行随机采样(注意它不是完全位于μ
处),最后对编码进行解码,最终的输出与训练实例类似。
从图中可以看出,尽管输入可能具有非常复杂的分布,但变分自编码器倾向于产生编码,看起来好像它们是从简单的高斯分布采样的:在训练期间,损失函数(将在下面讨论)推动 编码在编码空间(也称为潜在空间)内逐渐迁移以占据看起来像高斯点集成的云的大致(超)球形区域。 一个重要的结果是,在训练了一个变分自编码器之后,你可以很容易地生成一个新的实例:只需从高斯分布中抽取一个随机编码,对它进行解码就可以了!
那么让我们看看损失函数。 它由两部分组成。 首先是通常的重建损失,推动自编码器重现其输入(我们可以使用交叉熵来解决这个问题,如前所述)。 第二种是潜在的损失,推动自编码器使编码看起来像是从简单的高斯分布中采样,为此我们使用目标分布(高斯分布)与编码实际分布之间的 KL 散度。 数学比以前复杂一点,特别是因为高斯噪声,它限制了可以传输到编码层的信息量(从而推动自编码器学习有用的特征)。 幸运的是,这些方程简化为下面的潜在损失代码:
eps = 1e-10 # smoothing term to avoid computing log(0) which is NaN
latent_loss = 0.5 * tf.reduce_sum(
tf.square(hidden3_sigma) + tf.square(hidden3_mean)
- 1 - tf.log(eps + tf.square(hidden3_sigma)))
一种常见的变体是训练编码器输出γ= log(σ^2)
而不是σ
。 只要我们需要σ
,我们就可以计算σ= exp(2/γ)
。 这使得编码器可以更轻松地捕获不同比例的σ
,从而有助于加快收敛速度。 潜在损失结束会变得更简单一些:
latent_loss = 0.5 * tf.reduce_sum(
tf.exp(hidden3_gamma) + tf.square(hidden3_mean) - 1 - hidden3_gamma)
以下代码使用log(σ^2)
变体构建图 15-11(左)所示的变分自编码器:
n_inputs = 28 * 28 # for MNIST
n_hidden1 = 500
n_hidden2 = 500
n_hidden3 = 20 # codings
n_hidden4 = n_hidden2
n_hidden5 = n_hidden1
n_outputs = n_inputs
learning_rate = 0.001
with tf.contrib.framework.arg_scope(
[fully_connected],
activation_fn=tf.nn.elu,
weights_initializer=tf.contrib.layers.variance_scaling_initializer()):
X = tf.placeholder(tf.float32, [None, n_inputs])
hidden1 = fully_connected(X, n_hidden1)
hidden2 = fully_connected(hidden1, n_hidden2)
hidden3_mean = fully_connected(hidden2, n_hidden3, activation_fn=None)
hidden3_gamma = fully_connected(hidden2, n_hidden3, activation_fn=None)
hidden3_sigma = tf.exp(0.5 * hidden3_gamma)
noise = tf.random_normal(tf.shape(hidden3_sigma), dtype=tf.float32)
hidden3 = hidden3_mean + hidden3_sigma * noise
hidden4 = fully_connected(hidden3, n_hidden4)
hidden5 = fully_connected(hidden4, n_hidden5)
logits = fully_connected(hidden5, n_outputs, activation_fn=None)
outputs = tf.sigmoid(logits)
reconstruction_loss = tf.reduce_sum(
tf.nn.sigmoid_cross_entropy_with_logits(labels=X, logits=logits))
latent_loss = 0.5 * tf.reduce_sum(
tf.exp(hidden3_gamma) + tf.square(hidden3_mean) - 1 - hidden3_gamma)
cost = reconstruction_loss + latent_loss
optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate)
training_op = optimizer.minimize(cost)
init = tf.global_variables_initializer()