在上一讲中,笔者简单地介绍了变分自编码器的基本原理。本节及下一节,笔者将介绍一种特殊的自编码器模型——变分自编码器(Variational autoencoder,VAE)。作为生成模型的两座大山之一(变分自编码器VAE和生成对抗网络GAN),VAE特别适用于利用概念向量进行图像生成和编辑的任务。

     经典的自编码器由于本身是一种有损的数据压缩算法,在进行图像重构时不会得到效果最佳或者良好结构的潜在空间表达,VAE则不是将输入图像压缩伟潜在空间的编码,而是将图像转换为最常见的两个统计分布参数——均值和标准差。然后使用这两个参数来从分布中进行随机采样得到隐变量,对隐变量进行解码重构即可。这是VAE的一段相当简洁的描述,实际上,因为概率图本身的抽象性,笔者认为VAE不是一个容易理解模型。所以,本节笔者就和大家一起来详细了解一下VAE的原理与机制。

640?wx_fmt=jpeg

生成模型与分布变换

     在统计学习方法中,通过生成方法所学习到模型就是生成模型(generative model)(对应于判别方法和判别模型)。所谓生成方法,就是根据数据学习输入X和输出Y之间的联合概率分布,然后求出条件概率分布p(Y|X)作为预测模型的过程,这种模型便是生成模型。比如说咱们传统机器学习中的朴素贝叶斯模型和隐马尔可夫模型都是生成模型。

     具体到深度学习和图像领域,生成模型也可以概括为用概率方式描述图像的生成,通过对概率分布采样产生数据。深度学习领域的生成模型的目标一般都很简单:就是根据原始数据构建一个从隐变量Z生成目标数据Y的模型,只是各个模型有着不同的实现方法。从概率分布的角度来解释就是构建一个模型将原始数据的概率分布转换到目标数据的概率分布,目标就是原始分布和目标分布要越像越好。所以,从概率论的角度来看,生成模型本质上就是一种分布变换。

640?wx_fmt=jpeg

变分自编码器的原理

     VAE的直观理解如下图所示:

640?wx_fmt=png

我们先根据图中的流程简述一下VAE的技术原理:

  • 首先编码器模块将输入图像转换为表示潜在空间中的两个参数:均值和方差,这两个参数可以定义潜在空间中的一个正态分布

  • 然后从这个正态分布中进行随机采样

  • 最后由解码器模块将潜在空间中的采样点映射回原始输入图像,从而达到重构的目的

     虽然上图的思路足够清晰,但恐怕还不能真正理解得了变分自编码器。我们来重新捋一捋整个思路流程。

     假设我们有一批原始数据样本 {X1,…,Xn},可以用 X 来描述这个样本的总体,在X的分布 p(X) 知道的情况下,我们可以直接对 p(X) 这个概率分布进行采样,如果是这样的话,皆大欢喜,后面就没 VAE 什么事了。但事与愿违,正常情况下,原始样本的分布 p(X) 我们是不知道的。那我们只好退而求其次,看看能不能采用迂回的战术,通过对 p(X) 进行变换来推算 X 也行。于是我们可以将 p(X) 的分布表示为:

640?wx_fmt=png

     根据上式,p(X|Z) 描述了一个由 Z 来生成 X 的模型,而我们假设 Z 服从标准正态分布,也就是 p(Z)=N(0,I)。如果这条路能走得通的话,那么我们就可以先从标准正态分布中采样一个 Z,然后根据 Z 来算一个 X,这会是一个很优秀的生成模型。最后我们将这个模型结合自编码器进行表示,如下图所示:

640?wx_fmt=jpeg

     这幅图可以看作是 VAE 的一种更为直观的表达:通过对原始样本均值和方差的统计量计算,我们可以将数据编码成潜在空间的正态分布,然后对正态分布进行随机采样,将采样的结果进行解码结构,最后生成目标图像。一气呵成,皆大欢喜。其实不然,上图有一个关键问题在于:采样后的得到的Zk跟原始数据中的Xk是否还存在着一一对应的关系。这很关键,因为正是这种一一对应的关系才使得模型具备输入图像的重构能力。

     根据上面的表述,实际的 VAE 其实是对每一个原始样本 Xk 配置了一个专属的正态分布。为什么是专属?因为我们后面要训练一个生成器 X=g(Z),希望能够把从分布 p(Z|Xk) 采样出来的一个 Zk 还原为 Xk。如果假设 p(Z) 是正态分布,然后从 p(Z) 中采样一个 Z,那么我们怎么知道这个 Z 对应于哪个真实的 X 呢?现在 p(Z|Xk) 专属于 Xk,我们有理由说从这个分布采样出来的 Z 应该要还原到 Xk 中去。还有一个问题就是,我们要怎样找出每一个 Xk 专属正态分布 p(Z|Xk) 的均值和方差呢?很简单,用神经网络进行拟合即可,有时候深度学习就是这么的粗暴。这样一来,我们就可以将上图中的 VAE 示意图修改成这样了:

640?wx_fmt=jpeg

     原来真正的 VAE 长这样!好了,我们基本搞清楚了,VAE 通过神经网络将原始数据进行均值和方差的潜在空间表征,然后将其描述为正态分布,再根据正态分布进行采样。下面我们把目光聚焦到正态分布和采样上来。

     先来看正态分布。首先,我们希望重构 X,也就是最小化原始分布和目标分布之间的误差,但是这个重构过程受到噪声的影响,因为 Zk 是通过重新采样过的,不是直接由 encoder 算出来的。噪声的存在会增加数据重构的难度,但是我们知道均值和方差都在编码过程中由神经网络计算得到的,所以模型为了重构的更好,在这个过程中肯定会尽量让方差为向 0 靠近,但不能等于 0,等于 0 的话就失去了随机性,这样跟普通的自编码器就没什么区别了。

640?wx_fmt=jpeg

     VAE 给出的一个办法在于让所有的专属正态分布都向 p(Z|Xk) 都向标准正态分布 N(0,1) 看齐,于是有下式:

640?wx_fmt=png

     这样我们就能达到我们的先验假设:p(Z) 是标准正态分布。然后我们就可以放心地从 N(0,I) 中采样来生成图像了。所以说,VAE 为了使模型具有生成能力,模型要求每个 p(Z|Xk) 都努力向标准正态分布看齐。如下图所示:

640?wx_fmt=jpeg

     再来看采样。潜在空间表示为正态分布之后就是采样过程了。这里,VAE 的原始论文中提出一种参数复现(Reparameterization)的采样技巧。假设我们要从 p(Z|Xk) 中采样一个 Zk 出来,尽管我们知道了 p(Z|Xk) 是正态分布,但是均值方差都是靠模型算出来的,我们要靠这个过程反过来优化均值方差的模型,但是“采样”这个操作是不可导的,而采样的结果是可导的,于是我们利用了一个事实:从N(μ,σ^2) 中采样一个 Z,就相当于从 N(0,1) 中采样了一个 ε ,然后做一个 Z = μ+ ε*σ 的变换即可。如下图所示:

640?wx_fmt=png

     采样完了之后我们就可以用一个解码网络(生成器)来对采样结果进行解码重构了。

     最后一个细节就是 VAE 训练的损失函数。VAE的参数训练由两个损失函数来训练,一个是重构损失函数,该函数要求解码出来的样本与输入的样本相似(与之前的自编码器相同),第二项损失函数是学习到的隐分布与先验分布的KL距离,可以作为一个正则化损失。具体损失函数公式这里不展开细说。

     以上就是变分自编码器的基本和细节,当然还有很多内容笔者每有展开细谈,感兴趣的朋友可以找来 VAE 的原始论文进行研读。

变分的含义与VAE的本质

     虽然已基本搞清楚了变分自编码器的基本细节和原理,但还有一些问题指值得我们继续探讨。大家注意到我们这个模型的名字叫做变分自编码器,但是我们讲到现在,好像也没有碰到变分的概念。所以,这里我们得捋一捋变分的含义。

     什么是变分?应该学过泛函分析的朋友们都知道。所以,我们从泛函开始说起。大家都知道函数的概念在于变量之间的映射,输入一个数值返回的也是一个数值。而泛函则是函数的函数,所谓函数的函数,就是输入函数,返回的是数值,我们一般用积分的形式来表示泛函。这是函数与泛函的对比。

     另外我们都知道函数有微分这一概念。函数的微分就是关于自变量 x 发生变化时对应函数值的变化量。将微分的概念推广到泛函就是变分的含义。所谓变分,就是自变量函数 x(t) 发生变化时,对应泛函值的变化量。所以,简单而言,变分就是泛函的微分。我们都知道微分可以用来求函数的极值,那么相应的变分就可以用来求泛函的极值,研究泛函机制的方法就是所谓的变分法(Variational Method)。

     泛函和变分解释清楚了,那么 VAE 中好像确实没有用到变分啊?实际上,VAE 中的变分,在于损失函数推导过程利用了 KL 散度及其性质,而 KL 散度本身则是一个泛函:

640?wx_fmt=png

     也许这就是变分自编码器中变分的含义。

     作为自编码器的一种,VAE 有着自己的特殊性,但是其本质并不复杂。正如我们在文章开头所说的一样,VAE 的思想和框架其实很简单。VAE本质上就是在我们常规的自编码器的基础上,对 encoder 的结果(在VAE中对应着计算均值的网络)加上了“高斯噪声”,使得结果 decoder 能够对噪声有鲁棒性;而那个额外的 KL loss(目的是让均值为 0,方差为 1),事实上就是相当于对 encoder 的一个正则项,希望 encoder 出来的东西均有零均值。 而编码计算方差的网络的作用在于动态调节噪声的强度。到这里,变分自编码器的基本原理基本上就讲完了。最后一点内容,我们来看一下 keras 给出的 VAE 实现。

VAE 的 keras 实现

导入相关模块:

import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import norm
from keras.layers import Input, Dense, Lambda
from keras.models import Model
from keras import backend as K
from keras import metrics
from keras.datasets import mnist
from keras.utils import to_categorical

设置模型相关参数:

batch_size = 100
original_dim = 784
latent_dim = 2 
intermediate_dim = 256
epochs = 50
epsilon_std = 1.0
num_classes = 10

加载mnist数据集:

(x_train, y_train_), (x_test, y_test_) = mnist.load_data()
x_train = x_train.astype('float32') / 255.
x_test = x_test.astype('float32') / 255.
x_train = x_train.reshape((len(x_train), np.prod(x_train.shape[1:])))
x_test = x_test.reshape((len(x_test), np.prod(x_test.shape[1:])))
y_train = to_categorical(y_train_, num_classes)
y_test = to_categorical(y_test_, num_classes)

建立计算均值和方差的编码网络:

x = Input(shape=(original_dim,))
h = Dense(intermediate_dim, activation='relu')(x)
# 算p(Z|X)的均值和方差
z_mean = Dense(latent_dim)(h)
z_log_var = Dense(latent_dim)(h)

编码示意图:

640?wx_fmt=png

定义参数复现技巧函数和抽样层:

# 参数复现技巧
def sampling(args):
    z_mean, z_log_var = args
    epsilon = K.random_normal(shape=(K.shape(z_mean)[0], latent_dim), mean=0.,
                              stddev=epsilon_std)    
    return z_mean + K.exp(z_log_var / 2) * epsilon
    
# 重参数层,相当于给输入加入噪声
z = Lambda(sampling, output_shape=(latent_dim,))([z_mean, z_log_var])

定义模型解码部分,即生成器:

# 解码层,也就是生成器部分
decoder_h = Dense(intermediate_dim, activation='relu')
decoder_mean = Dense(original_dim, activation='sigmoid')
h_decoded = decoder_h(z)
x_decoded_mean = decoder_mean(h_decoded)

解码示意图:

640?wx_fmt=png

接下来实例化三个模型:

  • 一个端到端的自动编码器,用于完成输入信号的重构

  • 一个用于将输入空间映射为隐空间的编码器

  • 一个利用隐空间的分布产生的样本点生成对应的重构样本的生成器

# 端到端的vae模型
vae = Model(x, x_decoded_mean)
# 构建encoder,然后观察各个数字在隐空间的分布
encoder = Model(x, z_mean)
# 构建生成器
decoder_input = Input(shape=(latent_dim,))
_h_decoded = decoder_h(decoder_input)
_x_decoded_mean = decoder_mean(_h_decoded)
generator = Model(decoder_input, _x_decoded_mean)

定义 VAE 损失函数并进行训练:

# xent_loss是重构loss,kl_loss是KL loss
xent_loss = original_dim * metrics.binary_crossentropy(x, x_decoded_mean)
kl_loss = - 0.5 * K.sum(1 + z_log_var - K.square(z_mean) - K.exp(z_log_var), axis=-1)
vae_loss = K.mean(xent_loss + kl_loss)
# add_loss是新增的方法,用于更灵活地添加各种lossvae.add_loss(vae_loss)
vae.compile(optimizer='rmsprop', loss=None)
vae.summary()

vae.fit(x_train,
        shuffle=True,
        epochs=epochs,
        batch_size=batch_size,
        validation_data=(x_test, None))

模型概要:

640?wx_fmt=png

模型训练过程:

640?wx_fmt=png

因为变分编码器是一个生成模型,我们可以用它来生成新数字。我们可以从隐平面上采样一些点,然后生成对应的显变量,即MNIST的数字:

# 观察隐变量的两个维度变化是如何影响输出结果的
n = 15  
# figure with 15x15 digits
digit_size = 28
figure = np.zeros((digit_size * n, digit_size * n))
#用正态分布的分位数来构建隐变量对
grid_x = norm.ppf(np.linspace(0.05, 0.95, n))
grid_y = norm.ppf(np.linspace(0.05, 0.95, n))
for i, yi in enumerate(grid_x):    
    for j, xi in enumerate(grid_y):
        z_sample = np.array([[xi, yi]])
        x_decoded = generator.predict(z_sample)
        digit = x_decoded[0].reshape(digit_size, digit_size)
        figure[i * digit_size: (i + 1) * digit_size,
               j * digit_size: (j + 1) * digit_size] = digit

plt.figure(figsize=(10, 10))
plt.imshow(figure, cmap='Greys_r')
plt.show()

生成手写数字图片如下:

640?wx_fmt=png

参考资料:

Auto-Encoding Variational Bayes

paperweekly 苏剑林 变分自编码器VAE:原来是这么一回事

Tutorial on Variational Autoencoders

往期精彩:


一个数据科学从业者的学习历程

640?

640?wx_fmt=jpeg

长按二维码.关注机器学习实验室

640?wx_fmt=jpeg

Logo

更多推荐