Tracy's Studio.

VAE_变分自编码器

字数统计: 3.1k阅读时长: 12 min
2020/07/10 Share

简介

VAE(Variational Auto-Encoder,VAE)是一种生成网络。

假如我们有一个带有解卷积层的网络,我们设置输入为值全为1的向量,输出为一张图像。然后,我们可以训练这个网络去减小重构图像和原始图像的平均平方误差。那么训练完后,这个图像的信息就被保留在了网络的参数中。

带有解卷积层的网络

这次我们用one-hot向量而不是全1向量。我们用[1, 0, 0, 0]代表猫,用[0, 1, 0, 0]代表狗。虽然这要没什么问题,但是我们最多只能储存4张图片。当然,我们也可以增加向量的长度和网络的参数,那么我们可以获得更多的图片。

但是,这样的向量很稀疏。为了解决这个问题,我们想使用实数值向量而不是0,1向量。我们可认为这种实数值向量是原图片的一种编码,这也就引出了编码/解码的概念。举个例子,[3.3, 4.5, 2.1, 9.8]代表猫,[3.4, 2.1, 6.7, 4.2] 代表狗。这个已知的初始向量可以作为我们的潜在变量。

使用随机初始化来代表图片并不是一个好的手段,我们希望计算机能够帮我们实现自动编码。在Auto-Encoder模型中,我们加入了一个编码器,它能够帮助我们把图片编码程向量,然后解码器能够把这些向量恢复成图片。

带有编码器和解卷积层的网络

此时获得的网络是标准自编码器,能够将图片的编码向量进行存储,从而进行重构。

若对于编码器添加约束,强迫它产生服从单位高斯分布的潜在变量。这就是VAE不同于标准自编码器的地方。现在,产生新的图片也变得容易:我们只要从单位高斯分布中进行采样,然后把它传给解码器就可以了。

对于我们的损失函数,我们可以把这两方面进行加和。一方面,是图片的重构误差,我们可以用平均平方误差来度量,另一方面。我们可以用KL散度来度量我们潜在变量的分布和单位高斯分布的差异。

loss

KL散度可以求两个分布间的距离,KL越小,分布距离越近,即越相似。

loss

VAE的编码器会产生两个向量:一个是均值向量,一个是标准差向量。

ms

我们可以这样来计算KL散度:

1
# z_mean and z_stddev are two vectors generated by encoder network

latent_loss = 0.5 * tf.reduce_sum(tf.square(z_mean) + tf.square(z_stddev) - tf.log(tf.square(z_stddev)) - 1,1)

当我们计算解码器的loss时,我们就可以从标准差向量中采样,然后加到我们的均值向量上,就得到了编码去需要的潜在变量。

la

VAE除了能让我们能够自己产生随机的潜在变量,这种约束也能提高网络的产生图片的能力。

为了更加形象,我们可以认为潜在变量是一种数据的转换。

使用VAE好处就是可以通过编码解码的步骤,直接比较重建图片和原始图片的差异,但是GAN做不到。

另外,VAE的一个劣势就是没有使用对抗网络,所以会更趋向于产生模糊的图片。

方法

ex

p(X|Z) 就描述了一个由 Z 来生成 X 的模型,而我们假设 Z 服从标准正态分布,也就是 p(Z)=N(0,I)。如果这个理想能实现,那么我们就可以先从标准正态分布中采样一个 Z,然后根据 Z 来算一个 X,也是一个很棒的生成模型

接下来就是结合自编码器来实现重构,保证有效信息没有丢失,再加上一系列的推导,最后把模型实现。框架的示意图如下:

ms2

其实,在整个 VAE 模型中,我们并没有去使用 p(Z)(先验分布)是正态分布的假设,我们用的是假设 p(Z|X)(后验分布)是正态分布

具体来说,给定一个真实样本 Xk,我们假设存在一个专属于 Xk 的分布 p(Z|Xk)(学名叫后验分布),并进一步假设这个分布是(独立的、多元的)正态分布。

为什么要强调“专属”呢?因为我们后面要训练一个生成器 X=g(Z),希望能够把从分布 p(Z|Xk) 采样出来的一个 Zk 还原为 Xk。

如果假设 p(Z) 是正态分布,然后从 p(Z) 中采样一个 Z,那么我们怎么知道这个 Z 对应于哪个真实的 X 呢?现在 p(Z|Xk) 专属于 Xk,我们有理由说从这个分布采样出来的 Z 应该要还原到 Xk 中去

论文 Auto-Encoding Variational Bayes的应用部分,也特别强调了这一点:

for

那我怎么找出专属于 Xk 的正态分布 p(Z|Xk) 的均值和方差呢?好像并没有什么直接的思路。

那好吧,我就用神经网络来拟合出来

于是我们构建两个神经网络 μk=f1(Xk),logσ^2=f2(Xk) 来算它们了。我们选择拟合 logσ^2 而不是直接拟合 σ^2,是因为 σ^2 总是非负的,需要加激活函数处理,而拟合 logσ^2 不需要加激活函数,因为它可正可负。

到这里,我能知道专属于 Xk 的均值和方差了,也就知道它的正态分布长什么样了,然后从这个专属分布中采样一个 Zk 出来,然后经过一个生成器得到 X̂k=g(Zk)。

现在我们可以放心地最小化 D(X̂k,Xk)^2,因为 Zk 是从专属 Xk 的分布中采样出来的,这个生成器应该要把开始的 Xk 还原回来。于是可以画出 VAE 的示意图:

for2

事实上,VAE 是为每个样本构造专属的正态分布,然后采样来重构。

让我们来思考一下,根据上图的训练过程,最终会得到什么结果。

首先,我们希望重构 X,也就是最小化 D(X̂k,Xk)^2,但是这个重构过程受到噪声的影响,因为 Zk 是通过重新采样过的,不是直接由 encoder 算出来的。

显然噪声会增加重构的难度,不过好在这个噪声强度(也就是方差)通过一个神经网络算出来的,所以最终模型为了重构得更好,肯定会想尽办法让方差为0。

而方差为 0 的话,也就没有随机性了,所以不管怎么采样其实都只是得到确定的结果(也就是均值),只拟合一个当然比拟合多个要容易,而均值是通过另外一个神经网络算出来的。

说白了,模型会慢慢退化成普通的 AutoEncoder,噪声不再起作用

这样不就白费力气了吗?说好的生成模型呢?

别急别急,VAE 还让所有的 p(Z|X) 都向标准正态分布看齐,这样就防止了噪声为零,同时保证了模型具有生成能力。

如果所有的 p(Z|X) 都很接近标准正态分布 N(0,I),那么根据定义:

for3

这样我们就能达到我们的先验假设:p(Z) 是标准正态分布。然后我们就可以放心地从 N(0,I) 中采样来生成图像了。

for2

在VAE中使用了重参数的技巧(Reparameterization Trick)

for2

for2

本质

VAE 虽然也称是 AE(AutoEncoder)的一种,但它的做法(或者说它对网络的诠释)是别具一格的。

在 VAE 中,它的 Encoder 有两个,一个用来计算均值,一个用来计算方差。

它本质上就是在我们常规的自编码器的基础上,对 encoder 的结果(在VAE中对应着计算均值的网络)加上了“高斯噪声”,使得结果 decoder 能够对噪声有鲁棒性;而那个额外的 KL loss(目的是让均值为 0,方差为 1),事实上就是相当于对 encoder 的一个正则项,希望 encoder 出来的东西均有零均值。

那另外一个 encoder(对应着计算方差的网络)的作用呢?它是用来动态调节噪声的强度的。

直觉上来想,当 decoder 还没有训练好时(重构误差远大于 KL loss),就会适当降低噪声(KL loss 增加),使得拟合起来容易一些(重构误差开始下降)

反之,如果 decoder 训练得还不错时(重构误差小于 KL loss),这时候噪声就会增加(KL loss 减少),使得拟合更加困难了(重构误差又开始增加),这时候 decoder 就要想办法提高它的生成能力了

for2

重构的过程是希望没噪声的,而 KL loss 则希望有高斯噪声的,两者是对立的。所以,VAE 跟 GAN 一样,内部其实是包含了一个对抗的过程,只不过它们两者是混合起来,共同进化的

在 VAE 中,重构跟噪声是相互对抗的,重构误差跟噪声强度是两个相互对抗的指标,而在改变噪声强度时原则上需要有保持均值不变的能力,不然我们很难确定重构误差增大了,究竟是均值变化了(encoder的锅)还是方差变大了(噪声的锅)

参考资料:

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

VAE

代码

附上在tensorflow上实现VAE的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
import tensorflow as tf


# x_hat, n_hidden, dim_z, keep_prob
def gaussian_MLP_encoder(x, n_hidden, n_output, keep_prob):
# Gaussian MLP as encoder 构造编码器
# input: x(输入数据), n_hidden(隐藏层个数), n_output(输出层个数), keep_prob()
# output: mean(均值) ,stddev(方差)
print("encode:\n")
with tf.variable_scope("gaussian_MLP_encoder"):
# 初始化 w b
# scale=1.0,mode="fan_in",distribution="normal",seed=None,dtype=dtypes.float32
w_init = tf.contrib.layers.variance_scaling_initializer()
b_init = tf.constant_initializer(0.)

# 1st hidden layer
w0 = tf.get_variable('w0', [x.get_shape()[1], n_hidden], initializer=w_init)
b0 = tf.get_variable('b0', [n_hidden], initializer=b_init)
h0 = tf.matmul(x, w0) + b0
# elu为激活函数 <0 exp(x)-1 >0 =x
h0 = tf.nn.elu(h0)
h0 = tf.nn.dropout(h0, keep_prob)

# 2nd hidden layer 隐含层函数
w1 = tf.get_variable('w1', [h0.get_shape()[1], n_hidden], initializer=w_init)
b1 = tf.get_variable('b1', [n_hidden], initializer=b_init)
h1 = tf.matmul(h0, w1) + b1
h1 = tf.nn.tanh(h1)
h1 = tf.nn.dropout(h1, keep_prob)

# output layer 输出层
wo = tf.get_variable('wo', [h1.get_shape()[1], n_output * 2], initializer=w_init)
bo = tf.get_variable('bo', [n_output * 2], initializer=b_init)
gaussian_params = tf.matmul(h1, wo) + bo

# The mean parameter is unconstrained
mean = gaussian_params[:, :n_output]
# The standard deviation must be positive. Parametrize with a softplus and
# add a small epsilon for numerical stability
stddev = 1e-6 + tf.nn.softplus(gaussian_params[:, n_output:])

return mean, stddev


def bernoulli_MLP_decoder(z, n_hidden, n_output, keep_prob, reuse=False):
# Bernoulli MLP as decoder
# input: z(隐变量), n_hidden(隐藏层个数),n_output(输出个数), keep_prob(), reuse()
print("decode")
with tf.variable_scope("bernoulli_MLP_decoder", reuse=reuse):
# initializers
w_init = tf.contrib.layers.variance_scaling_initializer()
b_init = tf.constant_initializer(0.)

# 1st hidden layer
w0 = tf.get_variable('w0', [z.get_shape()[1], n_hidden], initializer=w_init)
b0 = tf.get_variable('b0', [n_hidden], initializer=b_init)
h0 = tf.matmul(z, w0) + b0
h0 = tf.nn.tanh(h0)
h0 = tf.nn.dropout(h0, keep_prob)

# 2nd hidden layer
w1 = tf.get_variable('w1', [h0.get_shape()[1], n_hidden], initializer=w_init)
b1 = tf.get_variable('b1', [n_hidden], initializer=b_init)
h1 = tf.matmul(h0, w1) + b1
h1 = tf.nn.elu(h1)
h1 = tf.nn.dropout(h1, keep_prob)

# output layer-mean
wo = tf.get_variable('wo', [h1.get_shape()[1], n_output], initializer=w_init)
bo = tf.get_variable('bo', [n_output], initializer=b_init)
y = tf.sigmoid(tf.matmul(h1, wo) + bo)

return y


def autoencoder(x_hat, x, dim_img, dim_z, n_hidden, keep_prob):
# input: x_hat(input), x(), dim_img(284**2), dim_z(-> n_output), n_hidden(500), keep_prob(0.9 dropout)
# encodingGateway
# 编码得到方差和均值
mu, sigma = gaussian_MLP_encoder(x_hat, n_hidden, dim_z, keep_prob)

# 得到
# sampling by re-parameterization technique
z = mu + sigma * tf.random_normal(tf.shape(mu), 0, 1, dtype=tf.float32)

# decoding
y = bernoulli_MLP_decoder(z, n_hidden, dim_img, keep_prob)
# 对数据的限制 y<1e-8 则y=1e-8 y>1-1e-8 则y=1-1e-8 否则y=原值
y = tf.clip_by_value(y, 1e-8, 1 - 1e-8)

# loss
marginal_likelihood = tf.reduce_sum(x * tf.log(y) + (1 - x) * tf.log(1 - y), 1)
KL_divergence = 0.5 * tf.reduce_sum(tf.square(mu) + tf.square(sigma) - tf.log(1e-8 + tf.square(sigma)) - 1, 1)

# 计算张量沿着指定的数轴上的平均值 用于降维或者计算tensor的平均值
marginal_likelihood = tf.reduce_mean(marginal_likelihood)
KL_divergence = tf.reduce_mean(KL_divergence)

ELBO = marginal_likelihood - KL_divergence

loss = -ELBO


return y, z, loss, -marginal_likelihood, KL_divergence


def decoder(z, dim_img, n_hidden):
y = bernoulli_MLP_decoder(z, n_hidden, dim_img, 1.0, reuse=True)
return y
CATALOG
  1. 1. 简介
  2. 2. 方法
  3. 3. 本质
  4. 4. 代码