欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

玩转自编码器:浅析变分自编码器(VAE)与条件变分自编码器(CVAE)

最编程 2024-07-24 10:00:26
...

在这里插入图片描述
使用通用自编码器的时候,首先将输入encoder压缩为一个小的 form,然后将其decoder转换成输出的一个估计。如果目标是简单的重现输入效果很好,但是若想生成新的对象就不太可行了,因为其实我们根本不知道这个网络所生成的编码具体是什么。虽然我们可以通过结果去对比不同的对象,但是要理解它内部的工作方式几乎是不可能的,甚至有时候可能连输入应该是什么样子的都不知道。

解决方法是用相反的方法使用变分自编码器(Variational Autoencoder,VAE),即不去关注隐含向量所服从的分布,只需要告诉网络我们想让这个分布转换为什么样子就行了。VAE对隐层的输出增加了长约束,而在对隐层的采样过程也能起到和一般 dropout 效果类似的正则化作用。而至于它的名字变分推理(Variational Inference,VI)的思想是使用一个简单化的模型代替原始复杂的模型,而且希望简单模型能尽可能的靠近原来那个复杂的模型,从而可以减少计算量。即最大化与数据点x相关联的变分下界来训练,即寻找一个容易处理的分布 q(z),使得 q(z) 与目标分布p(z|x) 尽量接近以便用q(z) 来代替 p(z|x),分布之间的 ‘接近’ 度量采用 Kullback–Leibler divergence(KL 散度)。

作为一个生成式模型的代表,需要对P(X|z)建模,即输入隐变量z,输出是已知观察变量X的概率。和判别模型类似,通过最大化后验概率来优化建模,即:
p ( z ∣ X ) = p ( X ∣ z ) p ( z ) p ( X ) = p ( X ∣ z ) p ( z ) ∫ z p ( X ∣ z ) p ( z ) d z p(z|X)=\frac {p(X|z)p(z)}{ p(X)}=\frac {p(X|z)p(z)}{\int_z p(X|z)p(z)dz} p(zX)=p(X)p(Xz)p(z)=zp(Xz)p(z)dzp(Xz)p(z)
但是在复杂模型和大规模数据面前,这个公式难以求解,于是利用变分的方法尝试找到一个q(z)函数来代替p(z|X)。那么要使两者尽可能的接近,即用衡量两者分布的相似度KL散度可以有:
K L ( q ( z ) ∣ ∣ p ( z ∣ X ) ) = ∫ q ( z ) l o g q ( z ) p ( z ∣ X ) d z = ∫ q ( z ) [ l o g q ( z ) − l o g p ( z ∣ X ) ] d z KL(q(z)||p(z|X))=\int q(z)log \frac{q(z)}{p(z|X)} dz=\int q(z)[logq(z)-logp(z|X)] dz KL(q(z)p(zX))=q(z)logp(zX)q(z)dz=q(z)[logq(z)logp(zX)]dz
在根据贝叶斯公式进行变换有:
= ∫ q ( z ) [ l o g q ( z ) − l o g p ( X ∣ z ) p ( z ) p ( X ) ] d z =\int q(z)[logq(z)-log \frac{p(X|z)p(z)}{p(X)}] dz =q(z)[logq(z)logp(X)p(Xz)p(z)]dz
= ∫ q ( z ) [ l o g q ( z ) − l o g p ( X ∣ z ) − l o g p ( z ) ] d z + l o g p ( X ) =\int q(z)[logq(z)-logp(X|z)-logp(z)]dz+logp(X) =q(z)[logq(z)logp(Xz)logp(z)]dz+logp(X)
X于要求解的z无关,恒等交换下可以得到:
l o g p ( X ) − K L ( q ( z ) ∣ ∣ p ( z ∣ X ) ) = ∫ q ( z ) l o g p ( X ∣ z ) d z − K L ( q ( z ) ∣ ∣ p ( z ) ) logp(X)-KL(q(z)||p(z|X))=\int q(z)logp(X|z)dz-KL(q(z)||p(z)) logp(X)KL(q(z)p(zX))=q(z)logp(Xz)dzKL(q(z)p(z))
等式左边关于可观察的X的分布p(X)不容易求,但是X是固定已知,P(X)是固定值实际上不需要考虑,那么如果想要-KL(q(z)||p(z|X))越小,即更接近的话,就要等式右边的值越大。而右边是q(z)的期望减去一个KL散度,于是优化目标就变成了:

  • 将第一项log似然期望(可以看为是重构损失)最大化
  • 将第二项的KL散度最小化

换个角度理解,其实

  • 第一项可以视为潜变量z的近似后验下可见和隐藏变量的联合对数似然性(正如EM一样,不同的是这里使用近似而不是精确后验)。
  • 第二项则可视为近似后验的熵。当 q 被选择为高斯分布,其中噪声被添加到预测平均值时,最大化该熵项促进增加该噪声的标准偏差。

回顾一下自编码器,Encoder模型实际实际上可以完成从观测数据到隐含数据的转变,那么就可以完全认为它学习到了相应的分布,KL散度的模型就建立好了。那么通过Encoder计算出的观察变量X所对应的隐变量z(即由X得到z,将通过网络来学习),直接建模,即输入z输出X,如果输出图像相近就可以认为似然函数也得到了最大化,这也就是Decoder的部分。

故整体来说,训练生成时,为了从模型生成样本,

  • VAE将会首先从编码分布 p m o d e l ( z ) p_{model}(z) pmodel(z) 中采样 z
  • 然后使样本通过可微生成器神经网络 g(z)
  • 最后,从分布 p m o d e l ( x ; g ( z ) ) = p m o d e l ( x ∣ z ) p_{model}(x; g(z)) = p_{model}(x | z) pmodel(x;g(z))=pmodel(xz) 中采样 x。

而在训练期间,近似推断网络(编码器)q(z | x) 用于获得 z, p m o d e l ( x ∣ z ) p_{model}(x | z) pmodel(xz) 则被视为解码器网络。

用一个简单的github代码实现来理解:这段代码目的是生成和MNIST中不一样的手写数字图像,而首先要做的先对MNIST中的数据进行编码,然后定义一个正态分布便于解码时得出我们期望生成的结果,即在解码时从该分布中随机采样得到“伪造”的图像。

import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
from tensorflow.examples.tutorials.mnist import input_data

mnist = input_data.read_data_sets('MNIST_data')#28*28的单色道图像数据

tf.reset_default_graph()

batch_size = 64

X_in = tf.placeholder(dtype=tf.float32, shape=[None, 28, 28], name='X')
Y    = tf.placeholder(dtype=tf.float32, shape=[None, 28, 28], name='Y')
Y_flat = tf.reshape(Y, shape=[-1, 28 * 28])#用于计算损失函数
keep_prob = tf.placeholder(dtype=tf.float32, shape=(), name='keep_prob')#dropout比率

dec_in_channels = 1
n_latent = 8 #隐变量的维度设置

reshaped_dim = [-1, 7, 7, dec_in_channels]
inputs_decoder = 49 * dec_in_channels // 2


def lrelu(x, alpha=0.3):#自定义Leaky ReLU函数使效果更好
    return tf.maximum(x, tf.multiply(x, alpha))

#编码
def encoder(X_in, keep_prob):
    activation = lrelu
    with tf.variable_scope("encoder", reuse=None):
        X = tf.reshape(X_in, shape=[-1, 28, 28, 1])
        x = tf.layers.conv2d(X, filters=64, kernel_size=4, strides=2, padding='same', activation=activation)
        x = tf.nn.dropout(x, keep_prob)
        x = tf.layers.conv2d(x, filters=64, kernel_size=4, strides=2, padding='same', activation=activation)
        x = tf.nn.dropout(x, keep_prob)
        x = tf.layers.conv2d(x, filters=64, kernel_size=4, strides=1, padding='same', activation=activation)
        x = tf.nn.dropout(x, keep_prob)
        x = tf.contrib.layers.flatten(x)
        
        #q(z|X),需要通过一批次的X来随机的生成z(z的条件分布也需要不断学习)
        mn = tf.layers.dense(x, units=n_latent) #利用计算means
        sd       = 0.5 * tf.layers.dense(x, units=n_latent) #利用计算standard         
        epsilon = tf.random_normal(tf.stack([tf.shape(x)[0], n_latent])) #从正态分布中采样
        z  = mn + tf.multiply(epsilon, tf.exp(sd)) 
        return z, mn, sd

#解码
def decoder(sampled_z, keep_prob):
    with tf.variable_scope("decoder", reuse=None):
        x = tf.layers.dense(sampled_z, units=inputs_decoder, activation=lrelu)
        x = tf.layers.dense(x, units=inputs_decoder * 2 + 1, activation=lrelu)
        x = tf.reshape(x, reshaped_dim)
        x = tf.layers.conv2d_transpose(x, filters=64, kernel_size=4, strides=2, padding='same', activation=tf.nn.relu)
        x = tf.nn.dropout(x, keep_prob)
        x = tf.layers.conv2d_transpose(x, filters=64, kernel_size=4, strides=1, padding='same', activation=tf.nn.relu)
        x = tf.nn.dropout(x, keep_prob)
        x = tf.layers.conv2d_transpose(x, filters=64, kernel_size=4, strides=1, padding='same', activation=tf.nn.relu)
        
        #还原成28x28
        x = tf.contrib.layers.flatten(x)
        x = tf.layers.dense(x, units=28*28, activation=tf.nn.sigmoid)
        img = tf.reshape(x, shape=[-1, 28, 28])
        return img

#结合
sampled, mn, sd = encoder(X_in, keep_prob)
dec = decoder(sampled, keep_prob)

#损失函数的计算由两部分组成
unreshaped = tf.reshape(dec, [-1, 28*28])
#最大似然
img_loss = tf.reduce_sum(tf.squared_difference(unreshaped, Y_flat), 1)
#KL散度
latent_loss = -0.5 * tf.reduce_sum(1.0 + 2.0 * sd - tf.square(mn) - tf.exp(2.0 * sd), 1)
loss = tf.reduce_mean(img_loss + latent_loss)
optimizer = tf.train.AdamOptimizer(0.0005).minimize(loss)

sess = tf.Session()
sess.run(tf.global_variables_initializer())

for i in range(30000):#开始训练
    batch = [np.reshape(b, [28, 28]) for b in mnist.train.next_batch(batch_size=batch_size)[0]]
    sess.run(optimizer, feed_dict = {X_in: batch, Y: batch, keep_prob: 0.8})
        
    if not i % 200:
        ls, d, i_ls, d_ls, mu, sigm = sess.run([loss, dec, img_loss, latent_loss, mn, sd], feed_dict = {X_in: batch, Y: batch, keep_prob: 1.0})
        plt.imshow(np.reshape(batch[0], [28, 28]), cmap='gray')
        plt.show()
        plt.imshow(d[0], cmap='gray')
        plt.show()
        print(i, ls, np.mean(i_ls), np.mean(d_ls))

#单独拿出来可以用于生成新的字符
randoms = [np.random.normal(0, 1, n_latent) for _ in range(10)]
imgs = sess.run(dec, feed_dict = {sampled: randoms, keep_prob: 1.0})
imgs = [np.reshape(imgs[i], [28, 28]) for i in range(len(imgs))]

for img in imgs:
    plt.figure(figsize=(1,1))
    plt.axis('off')
    plt.imshow(img, cmap='gray')

可以看到KL散度的计算为 0.5 ( − l o g σ 2 + μ 2 + σ 2 − 1 ) 0.5(-log\sigma^2+\mu^2+\sigma^2-1) 0.5(logσ2+μ2+σ21).

另外它还有非常好的特性是同时训练参数编码器与生成器网络的组合迫使模型学习编码器可以捕获可预测的坐标系,这使其成为一个优秀的流形学习算法,如下图展示的是由变分自动编码器学到的低维流形的例子,可以看出它发现了两个存在于面部图像的因素:旋转角和情绪表达。

这里写图片描述

而VAE的主要缺点是从在图像上训练的变分自动编码器中采样的样本往往有些模糊,而且原因尚不清楚,其中一种可能性是因为最小化KL散度而由于模糊性是最大似然的固有效应产生的。

同样作为生成模型的,也能得到很好的“伪造”数据的就是生成对抗GAN模型了。但与GAN十分的暴力(利用判别器做一个转化模块,而且衡量生成与真实的分布差异)不同,VAE不那么的直观,只是通过约束隐变量z服从标准正态分布以及重构数据(z与标准正态的KL散度和重构)实现了映射X=G(z)

重参数化技巧(Reparameterization Trick)
从一个分布中采样的时候因为采样这个动作是离散的,那么计算图就不能传递梯度,因此参数无法进行更新。此时的解决方案是将采样过程也作为输出,如上图,即此时的采样的计算变为 z = μ + ϵ σ z=\mu+\epsilon \sigma z=μ+ϵσ其中 ϵ \epsilon ϵ是从标准正太分布中采样出来的一个值,此时得到的z不再是固定的且可以通过梯度来更新了。
在这里插入图片描述
Gumbel-softmax技巧
上面的例子是在连续分布上的重参数方法,那么在离散情况下呢?理论上对于离散的我们也可以硬刚无限个点采样积分再求和,然后再梯度下降,但是显然在维度特别大的时候不可行。为此需要引入Gumbel max来解决,具体来说它会从离散的类别分布中采样(相当于加入Gumbel噪声,再取样): a r g m a x ( l o g p i − l o g ( − l o g ϵ i ) ) k , ϵ i ∈ U [ 0 , 1 ] arg max (log p_i-log(-log \epsilon_i))^k, \epsilon_i \in U[0,1] argmax(logpilog(logϵi))k,ϵiU[0,1] 即先算各个类别pk的对数,然后从均匀分布U中采样k个加进去,最后把最大值对应的类别抽取出来。此时可以看到随机性变成了在U采样的部分,这就是离散情况下的重参数。

但是argmax不可导,所以需要gumbel softmax这一光滑版本(其实argmax实际上就是one-hot,所以找它的近似就是softmax了): s o f t m a x ( l o g p i − l o g ( − l o g ϵ i ) τ ) k , ϵ i ∈ U [ 0 , 1 ] softmax (\frac{log p_i-log(-log \epsilon_i)}{\tau})^k, \epsilon_i \in U[0,1] softmax(τlogpilog(logϵi))k,ϵiU[0