深度学习第52讲:变分自编码器VAE原理以及keras实现,物联网嵌入式开发 400道面试题通关宝典助你进大厂

49 阅读9分钟

img img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上物联网嵌入式知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、电子书籍、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

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

往期精彩:

收集整理了一份《2024年最新物联网嵌入式全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升的朋友。 img img

如果你需要这些资料,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人

都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!