变分自编码器(VAE)学习笔记
摘抄并译自 CARL DOERSCH, 2016, Tutorial on Variational Autoencoders
简介
生成式模型是机器学习中对定义在高维空间 中的数据点 上的概率分布 进行建模的子领域,例如图片就是比较常见的用于生成式建模的数据,每个数据点(图片)有成千上百万个维度(像素),而生成式模型的任务就是以某种方式捕获各个维度之间的依赖关系。
我们希望依据现有的数据库,生成一些新的图片,这些图片很像现有的数据,但是又不完全相同,正式的说:我们想从未知的概率分布 中获取样本,我们的目标就是学习到一个模型 ,我们可以从这个学习到的模型中抽样,并且满足分布 和分布 尽可能的相似。
今年来神经网络这种普适函数近似器的蓬勃发展,使得用基于后向传播的函数近似器来构建生成式模型的框架结构变得很有前途,并且我们的VAE就是属于其中之一;我们需要作出的假设是很弱的(给定高容量的模型,由假设引入的误差非常的小),并且我们使用后向传播来进行快速训练。
基础知识:隐变量模型
当训练一个生成式模型的时候,数据空间各个维度之间的依赖度越高的时候,就越难以训练。假设我们想要生成手写数字0~9,如果生成的左半边是“8”的左半边,那么右半边将不能是其它字符的右半边,因为如果是那样的话,生成的完整字符将不是有意义的数字(作者意思是如果我们不按照任何规则胡乱生成,那么结果将是无意义的字符,而引出下面的隐变量)。直觉上,如果模型能首先“决定”要生成什么数字,然后再将具体的数值赋给一个个像素点,那将会起到帮助作用,这里的“决定”就是“隐变量”latent variable。例如,我们想要生成一个数字,那么我们可以先从0~9集合中随机抽一个数值 ,然后再确保生成的每一笔画都符合该数字。隐变量 之所以称之隐是因为当模型生成一个字符时,我们不是必须知道生成该字符的隐变量的具体设置,我们需要通过例如计算机视觉来推断它。
我们想要模型能充分表示我们的数据,那么首先需要确保对每个数据集中的样本 ,在隐变量中至少有一种的设置能够使得模型生成与样本 非常相似的结果。正式地说,我们有一个高维空间 中的隐变量 ,并且我们能够很容易地从定义在 空间中的累计概率分布 中抽样(因为我们要生成数据,所以需要容易抽样的隐变量概率分布函数)。然后,假设我们有一族确定性的函数 ,它是由参数空间 中的向量 来参数化的,有 。虽然函数 是确定性的,但是如果 是随机变量并且 是固定的,那么 就是 空间中的随机变量。我们希望优化参数 ,使得用 中抽样的z生成的目标 以高概率相似与现有的数据点 。更数学书化的表示是,最大化生成过程中训练集的每个样本的概率:
这里 被替换成了 ,这样我们就可以用全概率公式显示的使得 依赖 。其实我们用的这套框架叫做最大似然法,那么在这个框架背后的直觉就是,如果模型很可能生成训练集中的样本,那么它同样也很可能生成相似与训练集的样本,而不太可能生成不相似(训练样本)的一些样本。在VAE中,我们选择高斯分布作为输出分布,即: 。也就是说,输出分布是一个均值为 ,协方差矩阵为常数 乘上一个单位矩阵 的高斯分布( 是一个超参数)。当然输出分布也未必是高斯的,例如对于二值化的输出,就可以用Bernoulli分布,最简单的要求就是 可以被计算,并且关于 是连续的。
变分自编码器
其实呢,VAE在数学上并不像传统的自编码器,比如:去噪自编码器。VAE使用图(1)的模型近似地最大化公式(1)。它之所以被称为自编码器,是因为在训练阶段的目标函数,的确包涵一个编码器和一个解码器。
为了解决公式(1),VAE需要解决两个问题:1)怎样来定义隐变量
,2)如何来求在
上的积分。
如何来确定 的以至于可以用来捕获样本的隐藏表示?事实上,在神经网络如此普及的今天,我们当然希望模型自动的从数据集中寻找隐藏表示,而不是像以前那样,采用手动的构建表示(哪个维度表示什么信息,各个维度之间如何依赖,面对高纬度,这样的构造显得不切实际)。VAE采用了一种不寻常的方式,它假设各维度之间不存在简简单单的依赖关系,却断言 可以从简单的分布,例如标准正态分布中直接抽样。怎么保证呢?实际上,关键一点就是要认识到,定义在d维空间的任何分布,都可以通过一个足够复杂的函数作用于d个正态分布的变量来得到。假设我们用神经网络来作为函数近似器,那么我们可以想象网络的前几层可以把抽样的 变换到更有意义的特征表示,然后再使用这些隐藏表示来渲染出最终的输出样本。
解决了 的表示之后,我们再来解决 的计算问题,这里我们选择 。如果能找到 的一个可以计算公式,我们可以使用随机梯度上升法来训练模型,因为采用抽样 并计算 是不现实的,因为想要很好的近似 ,需要采样极其多的高维空间中的 。
确定目标函数
是否存在一个捷径使得我们可以使用抽样方法近似公式(1)呢?实际上,对大多数的 , 几乎接近0。所以对我们估计 没有什么帮助,事实上,变分自编码器的一个关键的思想就是,只抽样出那些更可能生成 的 ,再使用这些样本来计算 。这意味着我们需要一个新的函数 ,给定 的一个值,输出一个更可能生成 的 的分布,并希望 在函数 下的值空间远远小于先验 下的 的值空间。这使得我们能够相对容易的计算出 。然而,如果 是从任意一个分布 中抽样的,而不是简单的标准正态分布,那么该怎么优化 呢?我们首先将 和 联系起来。
和 之间的KL散度:
应用贝叶斯公式: ,再将跟分布 无关项 移出求期望外有:
进一步整理后得到:
在上面的推导过程中并没有假设分布 的形状,既然我们对推断 感兴趣,那么我们有理由构造依赖于 的分布函数 ,并且能使 取得很小的值,因此导出下面的公式,它在变分自编码器中起着核心的作用:
公式(5)可以这样理解,左边的是我们希望最大化的项: 加上一个误差项,并且误差在给定足够大能力的 之后会变得非常小;右边是我们可以通过梯度上升法来优化的项,并且我们可以看到,这两项有着自编码器的结构:函数 将 编码为 ,而 使用 来重构 。再仔细看看公式(5)的左边,我们最大化 的同时,最小化 ,然而,我们到目前为止对 的认识只是抽象的描述,即给定 ,它给出的 能很好地再生与 相似的样本,这是不可解析的计算出来的。但是,最大化第二项这个负散度,意味着促使 逼近 ,在给定足够大容量的函数 时,我们认为这是可以做到的;并且,在散度值为0的时候,我们将直接优化 ,此外,我们将直接计算 来代替 。
优化目标函数
有了上面的目标函数后,我们接下来采用基于梯度优化的方法优化它,这就需要我们先确定 的具体形式,通常的选择是 ,这里的 和 是关于 的确定性的参数化的函数,参数 是可以从数据中自动学习来的。在实际中,这两个函数同样也可以通过神经网络来学习到,并且我们会约束 为一个对角矩阵。如此选择主要是因为可以获得计算代价上的优势,并且右边项的计算也变得很清晰。右边第二项中两个高斯分布之间的 散度,可以闭式地计算出来。
原文直接给出下面的公式(6)和(7),咋一看有点懵,自己简要推导一下吧:
(5-1)
其中:
为了方便我使用 代替 ,将 代入公式(5-1)并化简得到:
(5-2)
计算(5-2)的第一项的积分:
计算(5-2)的第二项的积分,这里的 省略下标:
计算(5-2)的第三项的积分,这里
圆括号中的为矩阵下标,由于对称矩阵的逆矩阵也是对称的:
综上所述:
若选择
,则有下面化简的公式:
接下来要解决公式(5)右边的第一项,这需要更多的技巧。我们可以再次通过抽样来估计 ,但是要得到一个好的估计,需要使用函数 作用大量样本 。因此,作为使用随机梯度下降优化中的做法,我们直接使用一个样本 ,并使用 来近似 ,毕竟,我们已经打算在数据集 中的不同样本 上作随机梯度下降了。按照这样的讨论,那么我们优化的目标将是:
对公式(8)求导,求导运算可以和期望运算互换,进而可以直接作用于下面的公式:
如果我们每次抽取一个X样本,再根据 抽取 的一个样本,然后计算公式(9)的梯度,这样就获得了一个随机梯度,但是,我们可以将任意多的这样的随机梯度求均值,那么梯度均值会收敛到公式(8)。
然而这里存在一个很显著的问题。的确从上面的数学公式看来似乎没什么问题呀,但是因为 不仅仅依赖于函数P的参数,同时也依赖于函数Q的参数,但是上面的公式(9)中,这种依赖关系消失了,为了使VAE能有效的工作,有必要驱使 生成的隐编码能够让 可靠地解码。从另一个不同的视角看这个问题,我们可以认为公式(9)是类似于图(4-1)。在这个图中前向传播部分能够很好地工作,如果给定大量的 值,也能够产生准确的期望值。然而,在误差后向传播过程中,我们需要将误差通过一个抽样层进行传播,而抽样操作是非连续的,不可求导的,因此无法优化抽样层下面的部分(即 层)。基于后向传播的随机梯度下降法的随机性来自于输入层,而不是来自网络中间的随机节点(事实上,我们一般说的神经网络就是确定性的网络,网络的节点是普通变量之间的运算,还有一类网络是随机网络,例如马尔可夫随机场、玻耳兹曼机等,它们的节点都是随机变量,因此确定性网络内部的随机抽样层阻碍了梯度的后向传播)。
不过我们有个再参数化的方法来解决这个问题,它是通过将抽样层从网络内部移到网络输入层来实现的:为了从 中抽样(上面是先通过两个网络分别构建 函数,再通过 抽样层进行直接抽样),我们可以先从标准正态分布中抽样 (I为单位矩阵),然后再通过线性变换 生成 。因此,实际上我们求导的目标是:
上面这个公式可以用图(4-2)来刻画,注意到,两个需要求期望的分布函数都没有依赖模型的参数,所以我们可以安全的将求导运算移动到期望内而不会影响等价性。这下,给定一个固定的 和 ,上面的公式是确定性的,并且可以计算关于函数 对应的参数的导数了。
写吐了,下次再写吧(未完)
下面给出VAE的tensorflow一个实现:
import tensorflow as tf
import sys
import numpy as np
from scipy import misc
from tensorflow.examples.tutorials.mnist import input_data
# 代码几乎是将原作者的原caffe模型拿来用tensorflow实现的。
# 下面的函数copy自原作者的utils
def imtile(imlist,width=10,sep=2,brightness=1):
i=0;
imrows=[];
while(i<len(imlist)):
j=0;
imrow=[];
while(j<width):
imrow.append(imlist[i]);
j+=1;
if(j<width):
imrow.append(np.ones((imlist[i].shape[0],sep))*brightness);
if(len(imlist[0].shape)==3):
imrow[-1]=np.tile(imrow[-1][:,:,None],(1,1,imlist[0].shape[2]))
i+=1;
imrows.append(np.concatenate(imrow,axis=1))
if(i<len(imlist)):
imrows.append(np.ones((sep,imrows[-1].shape[1]))*brightness);
if(len(imlist[0].shape)==3):
imrows[-1]=np.tile(imrows[-1][:,:,None],(1,1,imlist[0].shape[2]))
return np.concatenate(imrows,axis=0);
mnist = input_data.read_data_sets('./MNIST_data')
# mu(X), sigma(X) 函数共享的参数
encoder_W1 = tf.Variable(tf.random_normal(shape=[784, 1000], stddev=0.1), dtype=tf.float32, name='encoder_W1')
encoder_W2 = tf.Variable(tf.random_normal(shape=[1000, 500], stddev=0.1), dtype=tf.float32, name='encoder_W2')
encoder_W3 = tf.Variable(tf.random_normal(shape=[500, 250], stddev=0.1), dtype=tf.float32, name='encoder_W3')
encoder_b1 = tf.Variable(tf.zeros(shape=[1000]), dtype=tf.float32, name='encoder_b1')
encoder_b2 = tf.Variable(tf.zeros(shape=[500]), dtype=tf.float32, name='encoder_b2')
encoder_b3 = tf.Variable(tf.zeros(shape=[250]), dtype=tf.float32, name='encoder_b3')
# mu(X), sigma(X) 函数独享的参数
mu_W = tf.Variable(tf.random_normal(shape=[250, 30], stddev=0.1), dtype=tf.float32, name='mu_W')
mu_b = tf.Variable(tf.zeros(shape=[30]), dtype=tf.float32, name='mu_b')
log_sigma_W = tf.Variable(tf.random_normal(shape=[250, 30], stddev=0.1), dtype=tf.float32, name='sigma_W')
log_sigma_b = tf.Variable(tf.zeros(shape=[30]), dtype=tf.float32, name='sigma_b')
X = tf.placeholder(tf.float32, shape=[None, 784], name='input_X')
X_scaled = X/255.
# 构建编码器
pre_encode_1 = tf.add(tf.matmul(X_scaled, encoder_W1), encoder_b1)
encode_1 = tf.nn.relu(pre_encode_1)
pre_encode_2 = tf.add(tf.matmul(encode_1, encoder_W2), encoder_b2)
encode_2 = tf.nn.relu(pre_encode_2)
pre_encode_3 = tf.add(tf.matmul(encode_2, encoder_W3), encoder_b3)
encode_3 = tf.nn.relu(pre_encode_3)
mu = tf.add(tf.matmul(encode_3, mu_W), mu_b)
# 由于方差必须是正的,所以可以用exp作用于log(sigma)上得到sigma,于是我们可以直接拟合“对数方差函数”
log_sigma = tf.add(tf.matmul(encode_3, log_sigma_W), log_sigma_b)
sigma = tf.exp(log_sigma)
# KL divergence
mu_norm_squared = tf.square(mu)
K = 1 #这里K为30,可以分摊到sum中的每一维,所以这里取1
trace_sigma = sigma # reduce操作放到后面一起计算
log_trace_sigma = log_sigma # 对角阵sigma的行列式的对数等于对每个对角元素的对数的求和,也放到一起计算
KLloss = 4*tf.reduce_mean(0.5 * tf.reduce_sum(mu_norm_squared+trace_sigma-K-log_trace_sigma, 1))
# 构建 z = mu + std * epsilon
# 标准正态分布抽样层
epsilon = tf.random_normal(shape=[tf.shape(X)[0], 30])
latent_z = mu + tf.sqrt(sigma)*epsilon
# 解码器的参数
decoder_W1 = tf.Variable(tf.random_normal(shape=[1000, 784], stddev=0.1), dtype=tf.float32, name='decoder_W1')
decoder_W2 = tf.Variable(tf.random_normal(shape=[500, 1000], stddev=0.1), dtype=tf.float32, name='decoder_W2')
decoder_W3 = tf.Variable(tf.random_normal(shape=[250, 500], stddev=0.1), dtype=tf.float32, name='decoder_W3')
decoder_W4 = tf.Variable(tf.random_normal(shape=[30, 250], stddev=0.1), dtype=tf.float32, name='decoder_W4')
decoder_b1 = tf.Variable(tf.zeros(shape=[784]), dtype=tf.float32, name='decoder_b1')
decoder_b2 = tf.Variable(tf.zeros(shape=[1000]), dtype=tf.float32, name='decoder_b2')
decoder_b3 = tf.Variable(tf.zeros(shape=[500]), dtype=tf.float32, name='decoder_b3')
decoder_b4 = tf.Variable(tf.zeros(shape=[250]), dtype=tf.float32, name='decoder_b4')
# 构建解码器
pre_decode_4 = tf.add(tf.matmul(latent_z, decoder_W4), decoder_b4)
decode_4 = tf.nn.relu(pre_decode_4)
pre_decode_3 = tf.add(tf.matmul(decode_4, decoder_W3), decoder_b3)
decode_3 = tf.nn.relu(pre_decode_3)
pre_decode_2 = tf.add(tf.matmul(decode_3, decoder_W2), decoder_b2)
decode_2 = tf.nn.relu(pre_decode_2)
pre_decode_1 = tf.add(tf.matmul(decode_2, decoder_W1), decoder_b1)
# 重构损失
reconstruction_loss = 0.5 * tf.reduce_mean(tf.reduce_sum(tf.pow(tf.subtract(pre_decode_1, X), 2.0)))
loss = KLloss + reconstruction_loss
train_op = tf.train.AdamOptimizer(learning_rate=0.001).minimize(loss)
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
for step in range(60000):
train_X, _ = mnist.train.next_batch(100)
kl_loss, rectr_loss, _ = sess.run([KLloss, reconstruction_loss, train_op], feed_dict={X: train_X})
sys.stdout.write("\rTrain step: %d, KL loss: %.3f, reconstruction loss: %.3f" %(step, kl_loss, rectr_loss))
if (step+1) % 1000 == 0:
recs = sess.run(pre_decode_1, feed_dict={latent_z:sess.run(tf.random_normal([20,30], mean=0, stddev=1))})
imlist = []
for i in recs:
imlist.append(np.reshape(i, (28,28)))
misc.imsave('recon/recon_%d.png' % (step+1), imtile(imlist))