精华内容
下载资源
问答
  • 卷积变分自动编码

    千次阅读 2019-03-16 09:57:56
    在生成网络中,我们通过使用完全连接的层,然后是三个卷积转置层(在某些情况下是反卷积层)来镜像这种架构。 注意,通常的做法是在训练VAE时避免使用批量标准化,因为使用小批量产生的额外随机性可能会加重采样...

    evolution of output during training

    This notebook demonstrates how to generate images of handwritten digits by training a Variational Autoencoder (1, 2).

    # to generate gifs
    !pip install imageio
    
    • Import TensorFlow and other libraries
    from __future__ import absolute_import, division, print_function
    
    !pip install tensorflow-gpu==2.0.0-alpha0
    import tensorflow as tf
    
    import os
    import time
    import numpy as np
    import glob
    import matplotlib.pyplot as plt
    import PIL
    import imageio
    
    from IPython import display
    
    • Load the MNIST dataset
      每个MNIST图像最初是784个整数的矢量,每个整数在0-255之间并且表示像素的强度。 我们在模型中使用伯努利分布对每个像素进行建模,并对数据集进行静态二值化。
    (train_images, _), (test_images, _) = tf.keras.datasets.mnist.load_data()
    
    train_images = train_images.reshape(train_images.shape[0], 28, 28, 1).astype('float32')
    test_images = test_images.reshape(test_images.shape[0], 28, 28, 1).astype('float32')
    
    # Normalizing the images to the range of [0., 1.]
    train_images /= 255.
    test_images /= 255.
    
    # Binarization
    train_images[train_images >= .5] = 1.
    train_images[train_images < .5] = 0.
    test_images[test_images >= .5] = 1.
    test_images[test_images < .5] = 0.
    
    TRAIN_BUF = 60000
    BATCH_SIZE = 100
    
    TEST_BUF = 10000
    
    • Use tf.data to create batches and shuffle the dataset
    train_dataset = tf.data.Dataset.from_tensor_slices(train_images).shuffle(TRAIN_BUF).batch(BATCH_SIZE)
    test_dataset = tf.data.Dataset.from_tensor_slices(test_images).shuffle(TEST_BUF).batch(BATCH_SIZE)
    
    • 用tf.keras.Sequential连接生成和推理网络
      在我们的VAE示例中,我们使用两个小型ConvNets用于生成和推理网络。 由于这些神经网络很小,我们使用tf.keras.Sequential来简化我们的代码。 在下面的描述中,分别用x和z表示观察和潜在变量。
    • Generative Network
      这定义了采用潜在编码作为输入的生成模型,并输出观察的条件分布的参数,即p(x | z)。 另外,我们使用单位高斯先验p(z)作为潜在变量。
    • Inference Network
      这定义了近似后验分布q(z | x),其将观察值作为输入并输出用于潜在表示的条件分布的一组参数。 在这个例子中,我们简单地将这个分布建模为对角高斯分布。 在这种情况下,推理网络输出分解高斯的均值和对数方差参数(对数方差而不是方差直接用于数值稳定性)。
    • Reparameterization Trick
      在优化过程中,我们可以从q(z | x)中采样,首先从单位高斯采样,然后乘以标准差并加上均值。 这确保了梯度可以通过样本传递到推理网络参数。
    • Network architecture
      对于推理网络,我们使用两个卷积层,然后是完全连接的层。 在生成网络中,我们通过使用完全连接的层,然后是三个卷积转置层(在某些情况下是反卷积层)来镜像这种架构。 注意,通常的做法是在训练VAE时避免使用批量标准化,因为使用小批量产生的额外随机性可能会加重采样随机性之上的不稳定性。
    class CVAE(tf.keras.Model):
      def __init__(self, latent_dim):
        super(CVAE, self).__init__()
        self.latent_dim = latent_dim
        self.inference_net = tf.keras.Sequential(
          [
              tf.keras.layers.InputLayer(input_shape=(28, 28, 1)),
              tf.keras.layers.Conv2D(
                  filters=32, kernel_size=3, strides=(2, 2), activation='relu'),
              tf.keras.layers.Conv2D(
                  filters=64, kernel_size=3, strides=(2, 2), activation='relu'),
              tf.keras.layers.Flatten(),
              # No activation
              tf.keras.layers.Dense(latent_dim + latent_dim),
          ]
        )
    
        self.generative_net = tf.keras.Sequential(
            [
              tf.keras.layers.InputLayer(input_shape=(latent_dim,)),
              tf.keras.layers.Dense(units=7*7*32, activation=tf.nn.relu),
              tf.keras.layers.Reshape(target_shape=(7, 7, 32)),
              tf.keras.layers.Conv2DTranspose(
                  filters=64,
                  kernel_size=3,
                  strides=(2, 2),
                  padding="SAME",
                  activation='relu'),
              tf.keras.layers.Conv2DTranspose(
                  filters=32,
                  kernel_size=3,
                  strides=(2, 2),
                  padding="SAME",
                  activation='relu'),
              # No activation
              tf.keras.layers.Conv2DTranspose(
                  filters=1, kernel_size=3, strides=(1, 1), padding="SAME"),
            ]
        )
    
      def sample(self, eps=None):
        if eps is None:
          eps = tf.random.normal(shape=(100, self.latent_dim))
        return self.decode(eps, apply_sigmoid=True)
    
      def encode(self, x):
        mean, logvar = tf.split(self.inference_net(x), num_or_size_splits=2, axis=1)
        return mean, logvar
    
      def reparameterize(self, mean, logvar):
        eps = tf.random.normal(shape=mean.shape)
        return eps * tf.exp(logvar * .5) + mean
    
      def decode(self, z, apply_sigmoid=False):
        logits = self.generative_net(z)
        if apply_sigmoid:
          probs = tf.sigmoid(logits)
          return probs
    
        return logits
    
    • Define the loss function and the optimizer
      VAE通过最大化边际对数似然下的证据下界(ELBO)进行训练:
      在这里插入图片描述
      在实践中,我们优化了单个样本蒙特卡罗对此期望的估计:
      在这里插入图片描述
      其中z从q(z | x)中采样。

    注意:我们也可以分析计算KL项,但为了简单起见,我们在蒙特卡罗估计中将所有三个项合并。

    def log_normal_pdf(sample, mean, logvar, raxis=1):
      log2pi = tf.math.log(2. * np.pi)
      return tf.reduce_sum(
          -.5 * ((sample - mean) ** 2. * tf.exp(-logvar) + logvar + log2pi),
          axis=raxis)
    
    def compute_loss(model, x):
      mean, logvar = model.encode(x)
      z = model.reparameterize(mean, logvar)
      x_logit = model.decode(z)
    
      cross_ent = tf.nn.sigmoid_cross_entropy_with_logits(logits=x_logit, labels=x)
      logpx_z = -tf.reduce_sum(cross_ent, axis=[1, 2, 3])
      logpz = log_normal_pdf(z, 0., 0.)
      logqz_x = log_normal_pdf(z, mean, logvar)
      return -tf.reduce_mean(logpx_z + logpz - logqz_x)
    
    def compute_gradients(model, x):
      with tf.GradientTape() as tape:
        loss = compute_loss(model, x)
      return tape.gradient(loss, model.trainable_variables), loss
    
    def apply_gradients(optimizer, gradients, variables):
      optimizer.apply_gradients(zip(gradients, variables))
      optimizer = tf.keras.optimizers.Adam(1e-4)
    
    • Training
      我们首先迭代数据集
      在每次迭代期间,我们将图像传递给编码器以获得近似后验q(z | x)的一组均值和对数方差参数。
      然后我们将重新参数化技巧应用于q(z | x)的样本
      最后,我们将重新参数化的样本传递给解码器,以获得生成分布p(x | z)的logits
      注意:由于我们使用训练集中60k数据点和测试集中10k数据点的keras加载的数据集,因此我们在测试集上得到的ELBO略高于使用Larochelle的MNIST动态二值化的文献中的报告结果。
    • Generate Images
      训练结束后,是时候生成一些图像了
      我们首先从单位高斯先验分布p(z)中采样一组潜在向量
      然后,生成器将潜在样本z转换为观察的logits,给出分布p(x | z)
      在这里我们绘制伯努利分布的概率
    epochs = 100
    latent_dim = 50
    num_examples_to_generate = 16
    
    # keeping the random vector constant for generation (prediction) so
    # it will be easier to see the improvement.
    random_vector_for_generation = tf.random.normal(
        shape=[num_examples_to_generate, latent_dim])
    model = CVAE(latent_dim)
    
    def generate_and_save_images(model, epoch, test_input):
      predictions = model.sample(test_input)
      fig = plt.figure(figsize=(4,4))
    
      for i in range(predictions.shape[0]):
          plt.subplot(4, 4, i+1)
          plt.imshow(predictions[i, :, :, 0], cmap='gray')
          plt.axis('off')
    
      # tight_layout minimizes the overlap between 2 sub-plots
      plt.savefig('image_at_epoch_{:04d}.png'.format(epoch))
      plt.show()
    
    generate_and_save_images(model, 0, random_vector_for_generation)
    
    for epoch in range(1, epochs + 1):
      start_time = time.time()
      for train_x in train_dataset:
        gradients, loss = compute_gradients(model, train_x)
        apply_gradients(optimizer, gradients, model.trainable_variables)
      end_time = time.time()
    
      if epoch % 1 == 0:
        loss = tf.keras.metrics.Mean()
        for test_x in test_dataset:
          loss(compute_loss(model, test_x))
        elbo = -loss.result()
        display.clear_output(wait=False)
        print('Epoch: {}, Test set ELBO: {}, '
              'time elapse for current epoch {}'.format(epoch,
                                                        elbo,
                                                        end_time - start_time))
        generate_and_save_images(
            model, epoch, random_vector_for_generation)
    
    • Display an image using the epoch number
    def display_image(epoch_no):
      return PIL.Image.open('image_at_epoch_{:04d}.png'.format(epoch_no))
    
    plt.imshow(display_image(epochs))
    plt.axis('off')# Display images
    
    • Generate a GIF of all the saved images.
    with imageio.get_writer('cvae.gif', mode='I') as writer:
      filenames = glob.glob('image*.png')
      filenames = sorted(filenames)
      last = -1
      for i,filename in enumerate(filenames):
        frame = 2*(i**0.5)
        if round(frame) > round(last):
          last = frame
        else:
          continue
        image = imageio.imread(filename)
        writer.append_data(image)
      image = imageio.imread(filename)
      writer.append_data(image)
        
    # this is a hack to display the gif inside the notebook
    os.system('cp cvae.gif cvae.gif.png')
    
    display.Image(filename="cvae.gif.png")
    
    展开全文
  • 变分自编码器原理 变分自编码器是一个从隐变量 ZZZ 生成目标数据 XXX 的模型。更准确地讲,它先假设 ZZZ 服从某些常见的分布(比如正态分布或均匀分布),然后希望训练一个模型 X=g(Z)X=g(Z)X=g(Z),这个模型能够将...

    变分自编码器原理

    变分自编码器是一个从隐变量 Z Z Z 生成目标数据 X X X 的模型。更准确地讲,它先假设 Z Z Z 服从某些常见的分布(比如正态分布或均匀分布),然后希望训练一个模型 X = g ( Z ) X=g(Z) X=g(Z),这个模型能够将原来的概率分布映射到训练集的概率分布,也就是说,其目的是进行分布之间的变换。
    在这里插入图片描述接下来,此文章将具体描述 V A E VAE VAE 的构建思路。

    第一阶段

    首先我们有一批数据样本 X 1 , … , X N {X_1,…,X_N} X1,,XN,其整体用 X X X 表示,我们可以根据 X 1 , … , X N {X_1,…,X_N} X1,,XN 得到 X X X 的分布 p ( X ) p(X) p(X),然后直接根据 p ( X ) p(X) p(X) 来采样,就可以得到所有可能的 X X X 了(包括 X 1 , … , X n {X1,…,Xn} X1,,Xn 以外的),这样一来,生成模型就实现了。
    但在实际上我们是得不到 X X X 的分布的,于是我们将 p ( X ) p(X) p(X) 重新定义为:
    p ( X ) = ∑ Z p ( X ∣ Z ) p ( Z ) p(X)=\sum_Zp(X|Z)p(Z) p(X)=Zp(XZ)p(Z)

    在上式中, p ( X ∣ Z ) p(X|Z) p(XZ) 是由 Z Z Z 生成 X X X 的模型,假设 Z Z Z 服从标准正态分布,也就是 p ( Z ) = N ( 0 , 1 ) p(Z)=N(0,1) p(Z)=N(0,1),那么我们就可以先从标准正态分布中采样一个 Z Z Z,然后根据 Z Z Z 来算一个 X X X,也可以达到生成目的。此时 V A E VAE VAE 的示意图为:
    在这里插入图片描述
    但是我们其实完全不清楚,究竟经过重新采样出来的 Z k Z_k Zk,是不是还对应着原来的 X k X_k Xk,所以我们如果直接最小化 D ( X ^ k , X k ) 2 D(\hat{X}_k,X_k)^2 D(X^k,Xk)2(这里 D D D 代表某种距离函数)是很不科学的。

    第二阶段

    其实,在整个 V A E VAE VAE 模型中,我们并没有去使用 p ( Z ) p(Z) p(Z)(先验分布)是正态分布的这个假设,而是假设 p ( Z ∣ X ) p(Z|X) p(ZX)(后验分布)是正态分布。
    具体来说,给定一个真实样本 X k X_k Xk,我们假设存在一个专属于 X k X_k Xk 的分布 p ( Z ∣ X k ) p(Z|X_k) p(ZXk)(学名叫后验分布),并进一步假设这个分布是(独立的、多元的)正态分布。
    所以现在 p ( Z ∣ X k ) p(Z|X_k) p(ZXk) 专属于 X k X_k Xk,我们有理由说从这个分布采样出来的 Z Z Z 应该要还原到 X k X_k Xk 中去。
    接下来,我们需要找出专属于样本 X k X_k Xk 的正态分布 p ( Z ∣ X k ) p(Z|X_k) p(ZXk) 的均值 μ \mu μ 和方差 σ 2 \sigma^2 σ2
    于是我们构建两个神经网络 μ k = f 1 ( X k ) \mu_k=f_1(X_k) μk=f1(Xk) l o g ( σ k 2 ) = f 2 ( X k ) log(\sigma_k^2)=f_2(X_k) log(σk2)=f2(Xk) 来算它们。之所以选择拟合 l o g ( σ k 2 ) log(\sigma_k^2) log(σk2) 而不是直接拟合 σ k 2 \sigma_k^2 σk2,是因为 σ k 2 \sigma_k^2 σk2 总是非负的,需要加激活函数处理,而拟合 l o g ( σ k 2 ) log(\sigma_k^2) log(σk2) 不需要加激活函数,因为它可正可负。
    得到 μ k \mu_k μk σ k 2 \sigma_k^2 σk2 之后, p ( Z ∣ X k ) p(Z|X_k) p(ZXk) 的正态分布就已经确定了,此时从其中采样一个 Z k Z_k Zk 出来,然后经过一个生成器得到 X ^ k = g ( Z k ) \hat{X}_k=g(Z_k) X^k=g(Zk)。这之后,我们可以放心地最小化 D ( X ^ k , X k ) 2 D(\hat{X}_k,X_k)^2 D(X^k,Xk)2 了,因为 Z k Z_k Zk 是从专属 X k X_k Xk 的分布中采样出来的,这个生成器应该要把开始的 X k X_k Xk 还原回来。于是此时 V A E VAE VAE 的示意图为:
    在这里插入图片描述事实上, V A E VAE VAE 是为每个样本构造专属的正态分布,然后采样来重构。

    第三阶段

    首先,我们希望重构 X X X,也就是最小化 D ( X ^ k , X k ) 2 D(\hat{X}_k,X_k)^2 D(X^k,Xk)2,但是这个重构过程受到噪声的影响,因为 Z k Z_k Zk 是通过重新采样得到的,而不是直接由 E n c o d e r Encoder Encoder 算出来的。
    显然噪声会增加重构的难度,不过好在这个噪声强度(也就是方差)是通过一个神经网络算出来的,所以最终模型为了重构得更好,肯定会想尽办法让方差为0。
    而方差为0的话,也就没有随机性了,所以不管怎么采样其实都只是得到确定的结果(也就是均值),只拟合一个当然比拟合多个要容易,而均值是通过另外一个神经网络算出来的。
    也就是说,模型会慢慢退化成普通的 A u t o E n c o d e r AutoEncoder AutoEncoder,噪声不再起作用。
    为了防止了噪声为零,同时保证模型具有生成能力, V A E VAE VAE 还让所有的 p ( Z ∣ X ) p(Z|X) p(ZX) 都向标准正态分布看齐。
    也就是说,如果所有的 p ( Z ∣ X ) p(Z|X) p(ZX) 都很接近标准正态分布 N ( 0 , 1 ) N(0, 1) N(0,1),那么根据定义:
    p ( Z ) = ∑ X p ( Z ∣ X ) p ( X ) = ∑ X N ( 0 , 1 ) p ( X ) = N ( 0 , 1 ) ∑ X p ( X ) = N ( 0 , 1 ) p(Z)=\sum_{X}p(Z|X)p(X)=\sum_{X}N(0, 1)p(X)=N(0, 1)\sum_{X}p(X)=N(0, 1) p(Z)=Xp(ZX)p(X)=XN(0,1)p(X)=N(0,1)Xp(X)=N(0,1)

    这样我们就能达到我们的先验假设: p ( Z ) p(Z) p(Z) 是标准正态分布。然后我们就可以放心地从 N ( 0 , 1 ) N(0, 1) N(0,1) 中采样来生成图像了。即:
    在这里插入图片描述
    那么,如何让所有的 p ( Z ∣ X ) p(Z|X) p(ZX) 都向 N ( 0 , 1 ) N(0, 1) N(0,1) 看齐呢?此时,我们使用一般(各分量独立的)正态分布与标准正态分布的 K L KL KL 散度 K L ( N ( μ , σ 2 ) ∣ ∣ N ( 0 , 1 ) ) KL(N(\mu, \sigma^2)||N(0, 1)) KL(N(μ,σ2)N(0,1)) 作为额外的 l o s s loss loss,计算结果为:
    L μ , σ 2 = 1 2 ∑ i = 1 d ( μ ( i ) 2 + σ ( i ) 2 − l o g ( σ ( i ) 2 ) − 1 ) L_{\mu, \sigma^2}=\frac{1}{2}\sum_{i=1}^{d}(\mu_{(i)}^2+\sigma_{(i)}^2-log(\sigma_{(i)}^2)-1) Lμ,σ2=21i=1d(μ(i)2+σ(i)2log(σ(i)2)1)
    上式中的 d d d 是隐变量 Z Z Z 的维度,而 μ ( i ) \mu_{(i)} μ(i) σ ( i ) 2 \sigma_{(i)}^2 σ(i)2分别代表一般正态分布的均值向量和方差向量的第 i i i 个分量。
    上式的推导过程:
    在这里插入图片描述整个结果分为三项积分,第一项实际上就是 − l o g ( σ 2 ) -log(\sigma^2) log(σ2) 乘以概率密度的积分(也就是 1),所以结果是 − l o g ( σ 2 ) -log(\sigma^2) log(σ2);第二项实际是正态分布的二阶矩,为 μ 2 + σ 2 \mu^2+\sigma^2 μ2+σ2;而根据定义,第三项实际上就是“减均值除以方差=1”。所以总结果就是:在这里插入图片描述

    第四阶段(重参数技巧)

    其实很简单,就是我们要从 p ( Z ∣ X k ) p(Z|X_k) p(ZXk) 中采样一个 Z k Z_k Zk 出来,尽管我们知道了 p ( Z ∣ X k ) p(Z|X_k) p(ZXk) 是正态分布,但是均值方差都是靠模型算出来的,我们要靠这个过程反过来优化均值方差的模型,但是“采样”这个操作是不可导的,而采样的结果是可导的,于是我们利用一个事实:从 N ( μ , σ 2 ) N(\mu, \sigma^2) N(μ,σ2) 中采样一个 Z Z Z,相当于从 N ( 0 , 1 ) N(0, 1) N(0,1) 中采样一个 ϵ \epsilon ϵ,然后让 Z = μ + ϵ × σ Z=\mu+\epsilon \times \sigma Z=μ+ϵ×σ。即:
    在这里插入图片描述
    所以,我们将从 N ( μ , σ 2 ) N(\mu, \sigma^2) N(μ,σ2) 采样变成了从 N ( 0 , 1 ) N(0, 1) N(0,1) 中采样,然后通过参数变换得到从 N ( μ , σ 2 ) N(\mu, \sigma^2) N(μ,σ2) 中采样的结果。这样一来,“采样”这个操作就不用参与梯度下降了,改为采样的结果参与,使得整个模型可训练了。

    VAE的本质

    V A E VAE VAE 虽然也是 A u t o E n c o d e r AutoEncoder AutoEncoder 的一种,但它的原理有所不同。
    V A E VAE VAE 中,它的 E n c o d e r Encoder Encoder 有两个,一个用来计算均值,一个用来计算方差,这是别具一格的。
    它本质上就是在我们常规的自编码器的基础上,对 E n c o d e r Encoder Encoder 的结果(在 V A E VAE VAE 中对应着计算均值的网络)加上了“高斯噪声”,使得结果 D e c o d e r Decoder Decoder 能够对噪声有鲁棒性;而那个额外的 K L l o s s KL loss KLloss(目的是让均值为 0,方差为 1),事实上就是相当于对 E n c o d e r Encoder Encoder 的一个正则项,希望 E n c o d e r Encoder Encoder 出来的东西均有零均值。
    那另外一个 E n c o d e r Encoder Encoder(对应着计算方差的网络)用来动态调节噪声的强度的。
    直觉上来想,当 D e c o d e r Decoder Decoder还没有训练好时(重构误差远大于 K L l o s s KL loss KLloss),就会适当降低噪声( K L l o s s KL loss KLloss 增加),使得拟合起来容易一些(重构误差开始下降)。
    反之,如果 D e c o d e r Decoder Decoder 训练得还不错时(重构误差小于 K L l o s s KL loss KLloss),这时候噪声就会增加( K L l o s s KL loss KLloss 减少),使得拟合更加困难了(重构误差又开始增加),这时候 D e c o d e r Decoder Decoder 就要想办法提高它的生成能力了。
    在这里插入图片描述

    代码实现

    1、导入需要的库

    import tensorflow as tf
    
    import numpy as np
    import matplotlib.pyplot as plt
    
    from IPython import display
    

    2、导入数据集

    (train_images, _), (test_images, _) = tf.keras.datasets.mnist.load_data()
    

    3、图像处理

    增加维度

    train_images = train_images.reshape(train_images.shape[0], 28, 28, 1).astype('float32')
    test_images = test_images.reshape(test_images.shape[0], 28, 28, 1).astype('float32')
    

    标准化

    # 标准化图片到区间 [0., 1.] 内
    train_images /= 255.
    test_images /= 255.
    

    二值化

    # 二值化
    train_images[train_images >= .5] = 1.
    train_images[train_images < .5] = 0.
    test_images[test_images >= .5] = 1.
    test_images[test_images < .5] = 0.
    

    使用 tf.data 来将数据分批和打乱

    TRAIN_BUF = 60000
    BATCH_SIZE = 100
    TEST_BUF = 10000
    
    train_dataset = tf.data.Dataset.from_tensor_slices(train_images).shuffle(TRAIN_BUF).batch(BATCH_SIZE)
    test_dataset = tf.data.Dataset.from_tensor_slices(test_images).shuffle(TEST_BUF).batch(BATCH_SIZE)
    

    4、建立CVAE模型

    class CVAE(tf.keras.Model):
        def __init__(self, latent_dim):
            super(CVAE, self).__init__()
            self.latent_dim = latent_dim
            self.inference_net = tf.keras.Sequential(
              [
                  tf.keras.layers.InputLayer(input_shape=(28, 28, 1)),
                  tf.keras.layers.Conv2D(
                      filters=32, kernel_size=3, strides=(2, 2), activation='relu'),
                  tf.keras.layers.Conv2D(
                      filters=64, kernel_size=3, strides=(2, 2), activation='relu'),
                  tf.keras.layers.Flatten(),
                  # No activation
                  tf.keras.layers.Dense(latent_dim + latent_dim),
              ]
            )
    
            self.generative_net = tf.keras.Sequential(
                [
                  tf.keras.layers.InputLayer(input_shape=(latent_dim,)),
                  tf.keras.layers.Dense(units=7*7*32, activation=tf.nn.relu),
                  tf.keras.layers.Reshape(target_shape=(7, 7, 32)),
                  tf.keras.layers.Conv2DTranspose(
                      filters=64,
                      kernel_size=3,
                      strides=(2, 2),
                      padding="SAME",
                      activation='relu'),
                  tf.keras.layers.Conv2DTranspose(
                      filters=32,
                      kernel_size=3,
                      strides=(2, 2),
                      padding="SAME",
                      activation='relu'),
                  # No activation
                  tf.keras.layers.Conv2DTranspose(
                      filters=1, kernel_size=3, strides=(1, 1), padding="SAME"),
                ]
            )
    
        def sample(self, eps=None):
            if eps is None:
                eps = tf.random.normal(shape=(100, self.latent_dim))
            return self.decode(eps, apply_sigmoid=True)
    
        def encode(self, x):
            mean, logvar = tf.split(self.inference_net(x), num_or_size_splits=2, axis=1)
            return mean, logvar
    
        def reparameterize(self, mean, logvar):
            eps = tf.random.normal(shape=mean.shape)
            return eps * tf.exp(logvar * .5) + mean
    
        def decode(self, z, apply_sigmoid=False):
            logits = self.generative_net(z)
            if apply_sigmoid:
                probs = tf.sigmoid(logits)
                return probs
    
            return logits
    

    其中,

    • latent_dim 是指要用多少个噪声点生成一张图像。
    • tf.keras.layers.Dense(latent_dim + latent_dim) 中,输出的维度之所以为两个 latent_dim,是因为我们希望 inference_net 的输出是均值和方差(的对数),所以 inference_net (也就是 Encoder)可以被看成两个神经网络。
    • sample() 函数用于将噪声输入 D e c o d e r Decoder Decoder,即 X ^ = g ( Z ) \hat{X}=g(Z) X^=g(Z) 的过程。
    • encode() 函数是编码过程,有两个输出,分别对应 μ k = f 1 ( X k ) \mu_k=f_1(X_k) μk=f1(Xk) l o g ( σ k 2 ) = f 2 ( X k ) log(\sigma_k^2)=f_2(X_k) log(σk2)=f2(Xk)。其中, μ k \mu_k μk l o g ( σ k 2 ) log(\sigma_k^2) log(σk2) 都是 latent_dim 维的向量。
    • reparameterize() 函数对应着重采样过程,即 Z = μ + ϵ × σ Z=\mu+\epsilon \times \sigma Z=μ+ϵ×σ,其中 ϵ \epsilon ϵ 是从 N ( 0 , 1 ) N(0, 1) N(0,1) 中采样出来的,经过从 ϵ \epsilon ϵ Z Z Z 的变换后得到的结果,与直接从 N ( μ , σ 2 ) N(\mu, \sigma^2) N(μ,σ2) 中采样一个 Z Z Z 一致。
    • decode() 函数即 sample() 函数的具体实现过程。

    5、初始化优化器

    optimizer = tf.keras.optimizers.Adam(1e-4)
    

    6、定义损失函数

    定义正态分布概率密度的对数

    正态分布的概率密度为:
    在这里插入图片描述
    所以其对数为:
    − 1 2 [ ( x − μ ) 2 σ 2 + l n ( 2 π σ 2 ) ] -\frac{1}{2}[\frac{(x-\mu)^2}{\sigma^2}+ln(2\pi \sigma^2)] 21[σ2(xμ)2+ln(2πσ2)]

    def log_normal_pdf(sample, mean, logvar, raxis=1):
        # 正态分布概率密度的对数
        log2pi = tf.math.log(2. * np.pi)
        return tf.reduce_sum(
            -.5 * ((sample - mean) ** 2. * tf.exp(-logvar) + logvar + log2pi),
            axis=raxis)
    

    计算损失

    经过第一部分的原理分析,在变分自编码器中,其损失除了常规的交叉熵损失之外,还有关于 K L KL KL 散度的损失。

    def compute_loss(model, x):
        mean, logvar = model.encode(x)
        z = model.reparameterize(mean, logvar)
        x_logit = model.decode(z)
    
        cross_ent = tf.reduce_sum(tf.nn.sigmoid_cross_entropy_with_logits(logits=x_logit, labels=x), axis=[1, 2, 3])
        KLD = -0.5 * tf.reduce_sum(1 + logvar- tf.pow(mean, 2) - tf.exp(logvar), axis=-1)
        return tf.reduce_mean(cross_ent + KLD)
    

    对以上代码中的变量进行分析

    取一批训练集样本
    for x in train_dataset.take(1):
        print(x.shape)
    
    (100, 28, 28, 1)
    

    即一批样本包含100个样本。

    将样本输入编码器
    model = CVAE(latent_dim)
    mean, logvar = model.encode(x)
    print(mean.shape)
    print(logvar.shape)
    
    (100, 50)
    (100, 50)
    

    即每个样本对应的正态分布的均值和方差(的对数)都是50维向量。

    重参数过程
    z = model.reparameterize(mean, logvar)
    print(z.shape)
    
    (100, 50)
    

    因为重参数过程中有 Z = μ + ϵ × σ Z=\mu+\epsilon \times \sigma Z=μ+ϵ×σ,所以每个采样样本 Z Z Z 的维度是和噪声维度相同的,即50维。

    将 Z 输入解码器
    x_logit = model.decode(z)
    print(x_logit.shape)
    
    (100, 28, 28, 1)
    

    即最终得到的形状和最早输入的形状一致。

    交叉熵损失
    cross_ent = tf.reduce_sum(tf.nn.sigmoid_cross_entropy_with_logits(logits=x_logit, labels=x), axis=[1, 2, 3])
    print(cross_ent.shape)
    
    (100,)
    

    其中的 axis=[1, 2, 3] 表示这个操作是在后三个维度上进行(因为 x 和 x_logit 都是四维的),即最终结果的形状将只剩下第一维度(即 [ 100 , 28 , 28 , 1 ] → [ 100 , ] [100, 28, 28, 1] \rightarrow [100, ] [100,28,28,1][100,])。

    KL散度损失
    KLD = -0.5 * tf.reduce_sum(1 + logvar- tf.pow(mean, 2) - tf.exp(logvar), axis=-1)
    print(KLD.shape)
    
    (100,)
    

    其中的 axis=-1 表示这个操作是在最后一个维度上进行( mean 和 logvar 都是二维的),即最终结果的形状将只剩下第一维度(即 [ 100 , 50 ] → [ 100 , ] [100, 50] \rightarrow [100, ] [100,50][100,])。

    7、定义梯度下降函数

    def compute_apply_gradients(model, x, optimizer):
        with tf.GradientTape() as tape:
            loss = compute_loss(model, x)
        gradients = tape.gradient(loss, model.trainable_variables)
        optimizer.apply_gradients(zip(gradients, model.trainable_variables))
    

    8、定义显示图像函数

    def generate_and_save_images(model, epoch, test_input):
        predictions = model.sample(test_input)
        fig = plt.figure(figsize=(4,4))
    
        for i in range(predictions.shape[0]):
            plt.subplot(4, 4, i+1)
            plt.imshow(predictions[i, :, :, 0], cmap='gray')
            plt.axis('off')
    
        # tight_layout 最小化两个子图之间的重叠
        plt.savefig('image_at_epoch_{:04d}.png'.format(epoch))
        plt.show()
    

    9、训练模型

    epochs = 100
    latent_dim = 50
    num_examples_to_generate = 16
    
    # 保持随机向量恒定以进行生成(预测),以便更易于看到改进。
    random_vector_for_generation = tf.random.normal(
        shape=[num_examples_to_generate, latent_dim])
    model = CVAE(latent_dim)
    
    generate_and_save_images(model, 0, random_vector_for_generation)
    
    for epoch in range(1, epochs + 1):
        for train_x in train_dataset:
            compute_apply_gradients(model, train_x, optimizer)
    
        if epoch % 1 == 0:
            loss = tf.keras.metrics.Mean()
            for test_x in test_dataset:
                loss(compute_loss(model, test_x))
            elbo = loss.result()
            display.clear_output(wait=False)
            print('Epoch: {}, Test set ELBO: {}, '.format(epoch,
                                                            elbo))
        generate_and_save_images(
            model, epoch, random_vector_for_generation)
    

    在这里插入图片描述

    参考资料

    变分自编码器VAE:原来是这么一回事 | 附开源代码

    展开全文
  • VAE ( Variational Autoencoders,变分自编码) 模型组合了神经网络和贝叶斯推理这两种最好的方法,是最酷的神经网络,已经成为无监督学习的流行方法之一。(摘自《TensorFlow 深度学习实战》 安东尼奥-古利;阿米塔-...

    1 编写目的

    VAE ( Variational Autoencoders,变分自编码) 模型组合了神经网络和贝叶斯推理这两种最好的方法,是最酷的神经网络,已经成为无监督学习的流行方法之一。(摘自《TensorFlow 深度学习实战》 安东尼奥-古利;阿米塔-卡普尔。机械工业出版社)。

    在上一篇博客 VAE 模型基本原理简单介绍 已经介绍基本原理,这里记录一下 VAE 模型的实现过程(必须声明:)

    本次实验采用的是 notebook,可以是自己电脑上安装的 jupyter notebook,也可以使用自己云服务器安装的,也可以考虑使用谷歌提供的 Colaboratory

    2 卷积 VAE 模型的实现与测试

    注:本节内容绝大部分是参考 https://tensorflow.google.cn/tutorials/generative/cvae (推荐阅读英文版),只是对一些部分做了一些补充解释

    推荐直接下载这个文件,下载地址。下载后请去除后面的 .txt ,因为这是一个 ipynb 文件。

    如果方便的话,可以直接访问 VAE demo,使用 谷歌提供的免费 GPU 。

    不能访问的话直接下载到本地,再运行。

    2.1 安装依赖

    1. 确定使用的是 tensorflow 2.x
      !pip show tensorflow
      
      如果当前安装的不是 tensorflow 2.x 的话,请输入以下命令安装:
      !pip install tensorflow==2.3.0 -i https://pypi.tuna.tsinghua.edu.cn/simple
      
    2. 安装 imageio
      !pip install imageio
      

    2.2 导入相关库与加载数据集

    import tensorflow as tf
    
    import os
    import time
    import numpy as np
    import glob
    import matplotlib.pyplot as plt
    import PIL
    import imageio
    
    from IPython import display
    
    (train_images, _), (test_images, _) = tf.keras.datasets.mnist.load_data()
    
    train_images = train_images.reshape(train_images.shape[0], 28, 28, 1).astype('float32')
    test_images = test_images.reshape(test_images.shape[0], 28, 28, 1).astype('float32')
    
    # 标准化图片到区间 [0., 1.] 内
    train_images /= 255.
    test_images /= 255.
    
    # 二值化
    train_images[train_images >= .5] = 1.
    train_images[train_images < .5] = 0.
    test_images[test_images >= .5] = 1.
    test_images[test_images < .5] = 0.
    
    # 使用 tf.data 来将数据分批和打乱
    train_dataset = tf.data.Dataset.from_tensor_slices(train_images).shuffle(TRAIN_BUF).batch(BATCH_SIZE)
    test_dataset = tf.data.Dataset.from_tensor_slices(test_images).shuffle(TEST_BUF).batch(BATCH_SIZE)
    

    2.3 VAE 模型

    准备工作做完了后,这里正式开始编写实现VAE模型。

    关于 VAE 模型的基本原理请参考一下 上一篇博客

    这里简单回顾一下, VAE 模型可以理解为 三部分

    • 编码网络(Encoder Network),也称 推断网络 。该 NN 用来生成隐变量的参数(隐变量由多个高斯分布组成)。对于隐变量 z z z,首先初始化时可以是标准高斯分布,然后通过这个 NN,通过不断计算后验概率 q ( z ∣ x ) q(z|x) q(zx) 来逐步确定高斯分布的参数(均值和方差)。
    • 隐变量(Latent Variable)。作为 Encoder 过程的产物,隐变量至少能够包含一些输入数据的信息(降维的作用),同时也应该具有生成类似数据的潜力。
    • 解码网络(Decoder Network),也称 生成网络。该 NN 用于根据隐变量生成数据,我们希望它既有能力还原 encoder 的数据,同时还能根据数据特征生成一些输入样本中不包含的数据。

    卷积
    这里使用两个小卷积层分别用于 Encoder 和 Decoder 。在文献中,这些网络也分别被称为 推断网络或识别网络 (inference/recognition)和生成模型。

    重参数化技巧 (Reparameterization Trick)

    • 训练过程中,为了生成样本 z z z 以便于 decoder 操作,我们可以从 encoder 生成的分布中进行采样。但是,由于反向传播无法通过随机节点,因此此采样操作会产生瓶颈。
    • 为了解决这个问题,我们使用了一个重新参数化的技巧。我们使用 decoder 参数和另一个参数 ε \varepsilon ε 近似z,如下所示:
      z = μ + σ   ⊙   ε z = \mu +\sigma \ \odot \ \varepsilon z=μ+σ  ε

    其中 μ \mu μ σ \sigma σ 表示高斯分布中的均值 和 标准差。它们可以从 decoder 输出中导出。可以认为 ε \varepsilon ε 是用来保持 z z z 随机性的随机噪声。我们从标准正态分布生成。

    • 现在的 z z z q ( z ∣ x ) q(z|x) q(zx) 生成(通过参数 μ \mu μ σ \sigma σ ε \varepsilon ε),这将使模型分别通过 μ \mu μ σ \sigma σ 在 encoder 中 反向传播梯度,同时通过 ε \varepsilon ε 保持 z z z 的随机性。

    网络结构 (Network architecture)

    对于 VAE 模型构建,

    • 在 Encoder NN中,使用两个卷积层和一个完全连接的层。、
    • 在 Decoder NN中,通过使用一个完全连接的层和三个卷积转置层来镜像这种结构。

    注意,在训练VAE时,通常避免使用批次标准化,因为使用小批量的额外随机性可能会加剧抽样随机性之外的不稳定性。

    class CVAE(tf.keras.Model):
      """Convolutional variational autoencoder."""
    
      def __init__(self, latent_dim):
        super(CVAE, self).__init__()
        self.latent_dim = latent_dim
        self.encoder = tf.keras.Sequential(
            [
                tf.keras.layers.InputLayer(input_shape=(28, 28, 1)),
                tf.keras.layers.Conv2D(
                    filters=32, kernel_size=3, strides=(2, 2), activation='relu'),
                tf.keras.layers.Conv2D(
                    filters=64, kernel_size=3, strides=(2, 2), activation='relu'),
                tf.keras.layers.Flatten(),
                # No activation
                tf.keras.layers.Dense(latent_dim + latent_dim),
            ]
        )
    
        self.decoder = tf.keras.Sequential(
            [
                tf.keras.layers.InputLayer(input_shape=(latent_dim,)),
                tf.keras.layers.Dense(units=7*7*32, activation=tf.nn.relu),
                tf.keras.layers.Reshape(target_shape=(7, 7, 32)),
                tf.keras.layers.Conv2DTranspose(
                    filters=64, kernel_size=3, strides=2, padding='same',
                    activation='relu'),
                tf.keras.layers.Conv2DTranspose(
                    filters=32, kernel_size=3, strides=2, padding='same',
                    activation='relu'),
                # No activation
                tf.keras.layers.Conv2DTranspose(
                    filters=1, kernel_size=3, strides=1, padding='same'),
            ]
        )
    
      @tf.function
      def sample(self, eps=None):
        if eps is None:
          eps = tf.random.normal(shape=(100, self.latent_dim))
        return self.decode(eps, apply_sigmoid=True)
    
      def encode(self, x):
        mean, logvar = tf.split(self.encoder(x), num_or_size_splits=2, axis=1)
        return mean, logvar
    
      def reparameterize(self, mean, logvar):
        eps = tf.random.normal(shape=mean.shape)
        return eps * tf.exp(logvar * .5) + mean
    
      def decode(self, z, apply_sigmoid=False):
        logits = self.decoder(z)
        if apply_sigmoid:
          probs = tf.sigmoid(logits)
          return probs
        return logits
    

    2.4 定义损失函数和优化器

    上篇博客中提到过,VAE 通过 log ⁡ p ( x ) \log p(x) logp(x) 极大似然 ELBO ( the evidence lower bound) 进行训练:
    log ⁡ p ( x ) ≥ ELBO = E q ( z ∣ x ) [ log ⁡ p ( x , z ) q ( z ∣ x ) ] . \log p(x) \ge \text{ELBO} = \mathbb{E}_{q(z|x)}\left[\log \frac{p(x, z)}{q(z|x)}\right]. logp(x)ELBO=Eq(zx)[logq(zx)p(x,z)].

    实际操作中,我们优化了这种单样本蒙特卡罗估计:
    log ⁡ p ( x ∣ z ) + log ⁡ p ( z ) − log ⁡ q ( z ∣ x ) , \log p(x| z) + \log p(z) - \log q(z|x), logp(xz)+logp(z)logq(zx),
    其中 z z z q ( z ∣ x ) q(z|x) q(zx) 中采样。

    optimizer = tf.keras.optimizers.Adam(1e-4)
    
    
    def log_normal_pdf(sample, mean, logvar, raxis=1):
      log2pi = tf.math.log(2. * np.pi)
      return tf.reduce_sum(
          -.5 * ((sample - mean) ** 2. * tf.exp(-logvar) + logvar + log2pi),
          axis=raxis)
    
    
    def compute_loss(model, x):
      mean, logvar = model.encode(x)
      z = model.reparameterize(mean, logvar)
      x_logit = model.decode(z)
      cross_ent = tf.nn.sigmoid_cross_entropy_with_logits(logits=x_logit, labels=x)
      logpx_z = -tf.reduce_sum(cross_ent, axis=[1, 2, 3])
      logpz = log_normal_pdf(z, 0., 0.)
      logqz_x = log_normal_pdf(z, mean, logvar)
      return -tf.reduce_mean(logpx_z + logpz - logqz_x)
    
    
    @tf.function
    def train_step(model, x, optimizer):
      """Executes one training step and returns the loss.
    
      This function computes the loss and gradients, and uses the latter to
      update the model's parameters.
      """
      with tf.GradientTape() as tape:
        loss = compute_loss(model, x)
      gradients = tape.gradient(loss, model.trainable_variables)
      optimizer.apply_gradients(zip(gradients, model.trainable_variables))
    

    2.5 训练模型与生成图片

    训练

    • 我们从迭代数据集开始。
    • 在每次迭代期间,我们将图像传递给编码器,以获得近似后验 q ( z ∣ x ) q(z|x) q(zx) 的一组均值和对数方差参数(log-variance parameters)。
    • 然后,我们应用 重参数化技巧 q ( z ∣ x ) q(z|x) q(zx) 中采样。
    • 最后,我们将重新参数化的样本传递给解码器,以获取生成分布 p ( x ∣ z ) p(x|z) p(xz) 的 logit。
    • 注意:由于我们使用的是由 keras 加载的数据集,其中训练集中有 6 万个数据点,测试集中有 1 万个数据点,因此我们在测试集上的最终 ELBO 略高于对 Larochelle 版 MNIST 使用动态二值化的文献中的报告结果。

      关于 logits 的 解释

    生成图片

    • 进行训练后,可以生成一些图片了。
    • 我们首先从单位高斯先验分布 p ( z ) p(z) p(z) 中采样一组潜在向量。
    • 随后生成器将潜在样本 z z z 转换为观测值的 logit,得到分布 p ( x ∣ z ) p(x|z) p(xz)
    • 这里我们画出伯努利分布的概率。
    epochs = 100
    # set the dimensionality of the latent space to a plane for visualization later
    latent_dim = 2
    num_examples_to_generate = 16
    
    # keeping the random vector constant for generation (prediction) so
    # it will be easier to see the improvement.
    random_vector_for_generation = tf.random.normal(
        shape=[num_examples_to_generate, latent_dim])
    model = CVAE(latent_dim)
    
    def generate_and_save_images(model, epoch, test_sample):
      mean, logvar = model.encode(test_sample)
      z = model.reparameterize(mean, logvar)
      predictions = model.sample(z)
      fig = plt.figure(figsize=(4, 4))
    
      for i in range(predictions.shape[0]):
        plt.subplot(4, 4, i + 1)
        plt.imshow(predictions[i, :, :, 0], cmap='gray')
        plt.axis('off')
    
      # tight_layout minimizes the overlap between 2 sub-plots
      plt.savefig('image_at_epoch_{:04d}.png'.format(epoch))
      plt.show()
    
    # Pick a sample of the test set for generating output images
    assert batch_size >= num_examples_to_generate
    for test_batch in test_dataset.take(1):
      test_sample = test_batch[0:num_examples_to_generate, :, :, :]
    generate_and_save_images(model, 0, test_sample)
    
    for epoch in range(1, epochs + 1):
      start_time = time.time()
      for train_x in train_dataset:
        train_step(model, train_x, optimizer)
      end_time = time.time()
    
      loss = tf.keras.metrics.Mean()
      for test_x in test_dataset:
        loss(compute_loss(model, test_x))
      elbo = -loss.result()
      display.clear_output(wait=False)
      print('Epoch: {}, Test set ELBO: {}, time elapse for current epoch: {}'
            .format(epoch, elbo, end_time - start_time))
      generate_and_save_images(model, epoch, test_sample)
    

    一百次循环后,生成的图片如下(感觉还可以):

    在这里插入图片描述
    同时可以生化 gif 图片来方便查看生成过程。

    anim_file = 'cvae.gif'
    
    with imageio.get_writer(anim_file, mode='I') as writer:
      filenames = glob.glob('image*.png')
      filenames = sorted(filenames)
      for filename in filenames:
        image = imageio.imread(filename)
        writer.append_data(image)
      image = imageio.imread(filename)
      writer.append_data(image)
    

    展示 gif 图片

    import tensorflow_docs.vis.embed as embed
    embed.embed_file(anim_file)
    

    在这里插入图片描述

    2.6 继续玩隐变量

    隐变量

    在这里插入图片描述

    2.7 下一步工作

    以上内容绝大部分是摘录于tensorflow 官网的 tutorials ,这里实现的是卷积 VAE 模型,简称(cVAE)。根据官网该文章, Next Step

    • 应该通过增大网络大小来优化输出效果。比如说可以把 Conv2DConv2DTranspose 的层数调成 512等等 ;
    • 可以使用不同的数据集查看效果,比如 CIFAR-10

    3 总结

    这里只是介绍一下如何快速体验一下 VAE 模型,模型的实现也是 tensorflow 官网提供的,对原来的文章做了简单的翻译工作,并且也加了一些自己的理解和补充。如果有更多好玩的实现demo,也将补充在这里。

    Smileyan
    2020.10.10 15:31

    展开全文

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 2,455
精华内容 982
关键字:

卷积变分自编码