变分自编码器(VAE)

Diederik Kingma 和 Max Welling 于 2014 年推出了另一类重要的自编码器,并迅速成为最受欢迎的自编码器类型之一:变分自编码器。

它们与我们迄今为止讨论的所有自编码器完全不同,特别是:

  • 它们是概率自编码器,意味着即使在训练之后,它们的输出部分也是偶然确定的(相对于仅在训练过程中使用随机性的自编码器的去噪)。
  • 最重要的是,它们是生成自编码器,这意味着它们可以生成看起来像从训练集中采样的新实例。

这两个属性使它们与 RBM 非常相似(见附录 E),但它们更容易训练,并且取样过程更快(在 RBM 之前,您需要等待网络稳定在“热平衡”之后才能进行取样一个新的实例)

我们来看看他们是如何工作的。 图 15-11(左)显示了一个变分自编码器。 当然,您可以认识到所有自编码器的基本结构,编码器后跟解码器(在本例中,它们都有两个隐藏层),但有一个转折点:不是直接为给定的输入生成编码 ,编码器产生平均编码μ和标准差σ。 然后从平均值μ和标准差σ的高斯分布随机采样实际编码。 之后,解码器正常解码采样的编码。 该图的右侧部分显示了一个训练实例通过此自编码器。 首先,编码器产生μσ,随后对编码进行随机采样(注意它不是完全位于μ处),最后对编码进行解码,最终的输出与训练实例类似。

变分自编码器(VAE) - 图1

从图中可以看出,尽管输入可能具有非常复杂的分布,但变分自编码器倾向于产生编码,看起来好像它们是从简单的高斯分布采样的:在训练期间,损失函数(将在下面讨论)推动 编码在编码空间(也称为潜在空间)内逐渐迁移以占据看起来像高斯点集成的云的大致(超)球形区域。 一个重要的结果是,在训练了一个变分自编码器之后,你可以很容易地生成一个新的实例:只需从高斯分布中抽取一个随机编码,对它进行解码就可以了!

那么让我们看看损失函数。 它由两部分组成。 首先是通常的重建损失,推动自编码器重现其输入(我们可以使用交叉熵来解决这个问题,如前所述)。 第二种是潜在的损失,推动自编码器使编码看起来像是从简单的高斯分布中采样,为此我们使用目标分布(高斯分布)与编码实际分布之间的 KL 散度。 数学比以前复杂一点,特别是因为高斯噪声,它限制了可以传输到编码层的信息量(从而推动自编码器学习有用的特征)。 幸运的是,这些方程简化为下面的潜在损失代码:

  1. eps = 1e-10 # smoothing term to avoid computing log(0) which is NaN
  2. latent_loss = 0.5 * tf.reduce_sum(
  3. tf.square(hidden3_sigma) + tf.square(hidden3_mean)
  4. - 1 - tf.log(eps + tf.square(hidden3_sigma)))

一种常见的变体是训练编码器输出γ= log(σ^2)而不是σ。 只要我们需要σ,我们就可以计算σ= exp(2/γ)。 这使得编码器可以更轻松地捕获不同比例的σ,从而有助于加快收敛速度。 潜在损失结束会变得更简单一些:

  1. latent_loss = 0.5 * tf.reduce_sum(
  2. tf.exp(hidden3_gamma) + tf.square(hidden3_mean) - 1 - hidden3_gamma)

以下代码使用log(σ^2)变体构建图 15-11(左)所示的变分自编码器:

  1. n_inputs = 28 * 28 # for MNIST
  2. n_hidden1 = 500
  3. n_hidden2 = 500
  4. n_hidden3 = 20 # codings
  5. n_hidden4 = n_hidden2
  6. n_hidden5 = n_hidden1
  7. n_outputs = n_inputs
  8. learning_rate = 0.001
  9. with tf.contrib.framework.arg_scope(
  10. [fully_connected],
  11. activation_fn=tf.nn.elu,
  12. weights_initializer=tf.contrib.layers.variance_scaling_initializer()):
  13. X = tf.placeholder(tf.float32, [None, n_inputs])
  14. hidden1 = fully_connected(X, n_hidden1)
  15. hidden2 = fully_connected(hidden1, n_hidden2)
  16. hidden3_mean = fully_connected(hidden2, n_hidden3, activation_fn=None)
  17. hidden3_gamma = fully_connected(hidden2, n_hidden3, activation_fn=None)
  18. hidden3_sigma = tf.exp(0.5 * hidden3_gamma)
  19. noise = tf.random_normal(tf.shape(hidden3_sigma), dtype=tf.float32)
  20. hidden3 = hidden3_mean + hidden3_sigma * noise
  21. hidden4 = fully_connected(hidden3, n_hidden4)
  22. hidden5 = fully_connected(hidden4, n_hidden5)
  23. logits = fully_connected(hidden5, n_outputs, activation_fn=None)
  24. outputs = tf.sigmoid(logits)
  25. reconstruction_loss = tf.reduce_sum(
  26. tf.nn.sigmoid_cross_entropy_with_logits(labels=X, logits=logits))
  27. latent_loss = 0.5 * tf.reduce_sum(
  28. tf.exp(hidden3_gamma) + tf.square(hidden3_mean) - 1 - hidden3_gamma)
  29. cost = reconstruction_loss + latent_loss
  30. optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate)
  31. training_op = optimizer.minimize(cost)
  32. init = tf.global_variables_initializer()