Python深度学习之VAE
Deep Learning with Python
这篇文章是我学习《Deep Learning with Python》(第二版,François Chollet 著) 时写的系列笔记之一。文章的内容是从 Jupyter notebooks 转成 Markdown 的,你可以去 GitHub 或 Gitee 找到原始的 .ipynb
笔记本。
你可以去这个网站在线阅读这本书的正版原文(英文)。这本书的作者也给出了配套的 Jupyter notebooks。
本文为 第8章 生成式深度学习 (Chapter 8. Generative deep learning) 的笔记之一。
[TOC]
8.4 Generating images with variational autoencoders
用变分自编码器生成图像
前两篇介绍的 DeepDream 和 Neural Style Transfer 都只是有限地“修改”现有作品。而下面我们要介绍地 GAN 和 VAE 则是更加富有创造性的,这两种技术都是从图像的潜在空间中采样,并创建全新图像或编辑现有图像。
- VAE:变分自编码器(Variational AutoEncoder)
- GAN:生成式对抗网络(Generative Adversarial Network)
从潜在空间采样
潜在空间(latent space)是一个向量空间,其中任意点都可以被映射为一张逼真的图像。而实现这种映射(潜在点->图像)的模块就是 GAN 的 generator,或者 VAE 的 decoder。
GAN、VAE 生成图像的关键就在于找到一个低维的「表示潜在空间」(latent space of representations)。一旦找到这样的潜在空间,从中采样,映射到图像空间,就可以生成全新的图像。
GAN 和 VAE 学习的潜在空间有很大的区别:
- VAE 善于学习具有良好结构的潜在空间,其中的特定方向可以编码(表示)数据中一个有意义的变化的轴。
- GAN 生成的图像可以非常逼真,但潜在空间缺乏良好的结构、没有足够的连续性。
概念向量
概念向量(concept vector):给定一个表示的潜在空间或一个嵌入空间,空间中的特定方向可能表示原始数据中有意义的变化轴。例如对于图像,人脸图像的潜在空间中可能存在一个代表「微笑」这个概念的向量(称为微笑向量,smile vector):对于代表某张人脸的潜在点 z,z+s 就是同一张人脸面带微笑的表示。
找到了这样的一些概念向量之后,我们就可以用这种方法来编辑图像了:将图像投射到潜在空间,和概念向量做运算来移动其表示,然后再解码到图像空间,就可以改变图像中的某一概念了——比如微笑程度:
变分自编码器
自编码器是一种网络类型,接收一张图像,通过 encoder 模块将其映射到「潜在空间」,然后再通过 decoder 模块将其解码成与原始图像尺寸相同的输出。这东西训练时的目标是使输出和输入相同,所以我们把输入、输出用同一张图片。所以自编码器学习的是对原始输入进行重新构建。
通过对编码(编码器的输出)施加限制,可以让自编码器学到有用的数据潜在表示。比如限制编码要低维并且是稀疏的,这样编码器就可以将输入数据压缩为更少二进制位的信息:
变分自编码器 VAE,是一种现代化的自编码器。它是一种生成式模型,特别做利用概念向量进行图像编辑的任务。比起经典自编码器,VAE 可以学习更连续的、高度结构化的潜在空间。
VAE 不是将输入图像压缩成潜在空间中的固定编码,而是将图像转换为统计分布的参数——平均值和方差。VAE 解码的时候利用平均值和方差,从分布中随机采样一个元素,并将这个元素解码到原始输入。所以 VAE 的编码/解码过程是有一定的随机性的。
这个过程的随机性提高了 VAE 潜在空间的稳健性:VAE 需保证潜在空间采样的每个点都能解码为有效的输出,这迫使潜在空间的任何位置都对应有意义的表示。
上图展现了 VAE 的工作原理:
- Encoder 模块将输入样本
input_img
转换为表示潜在空间中的参数z_mean
和z_log_variance
; - 从这个潜在正态分布中随机采样一个点 z:
z = z_mean + exp(z_log_variance) * epsilon
,其中 epsilon 是取值很小的随机张量; - Decoder 模块将这个潜在点映射回原始输入图像。
epsilon 是随机的,所以需要与 input_img 编码的潜在位置(z-mean)靠近的每个点都能被解码为与 input_img 类似的图像,这个性质迫使潜在空间能够连续地有意义:潜在空间中任意两个相邻的点都会被解码为高度相似的图像。连续性以及潜在空间的低维度,又迫使潜在空间中的每个方向都表示数据中一个有意义的变化轴,这样就可以通过概念向量来进行操作。
用 Keras 实现 VAE 的伪代码如下:
1 | z_mean, z_log_variance = encoder(input_img) |
训练 VAE 需要两个损失函数:
- 重构损失(reconstruction loss):使解码后的样本匹配初始输入;
- 正则化损失(regularization loss):使潜在空间具有良好结构(连续性、概念向量可用性),同时也降低在训练数据上的过拟合;
我们具体实现编码器(encoder)网络:通过一个卷积神经网络,将输入图像 x 映射为两个向量 z_mean 和 z_log_var。
1 | # 不使用及时执行模式 |
1 | # VAE 编码器网络 |
接下来的代码将使用 z_mean 和 z_log_var 来生成(采样)一个潜在空间点 z。
1 | # 潜在空间采样的函数 |
然后是解码器的实现:将向量 z 的尺寸调整为图像大小,然后使用几个卷积层来得到最终的图像输出。
1 | # VAE 解码器网络 |
VAE 要用两个损失,所以不能直接写成 loss(input, target)
,我们需要编写一个自定义层,在其中使用内置的 add_loss
方法来创建需要的损失。
1 | # 用于计算 VAE 损失的自定义层 |
最后,将模型实例化并开始训练。由于我们的损失以及包含在自定义层里面了,所以编译时无须指定外部损失(loss=None
),所以也就不需要外部指定的目标数据(y=None
)。
这里我们用 MNIST 去训练它,也就是生成手写数字的潜在空间。
1 | from tensorflow.keras.datasets import mnist |
WARNING:tensorflow:Output custom_variational_layer_1 missing from loss dictionary. We assume this was done on purpose. The fit and evaluate APIs will not be expecting any data to be passed to custom_variational_layer_1.
Model: "functional_7"
__________________________________________________________________________________________________
Layer (type) Output Shape Param # Connected to
==================================================================================================
input_1 (InputLayer) [(None, 28, 28, 1)] 0
__________________________________________________________________________________________________
conv2d_5 (Conv2D) (None, 28, 28, 32) 320 input_1[0][0]
__________________________________________________________________________________________________
conv2d_6 (Conv2D) (None, 14, 14, 64) 18496 conv2d_5[0][0]
__________________________________________________________________________________________________
conv2d_7 (Conv2D) (None, 14, 14, 64) 36928 conv2d_6[0][0]
__________________________________________________________________________________________________
conv2d_8 (Conv2D) (None, 14, 14, 64) 36928 conv2d_7[0][0]
__________________________________________________________________________________________________
flatten_1 (Flatten) (None, 12544) 0 conv2d_8[0][0]
__________________________________________________________________________________________________
dense_4 (Dense) (None, 32) 401440 flatten_1[0][0]
__________________________________________________________________________________________________
dense_5 (Dense) (None, 2) 66 dense_4[0][0]
__________________________________________________________________________________________________
dense_6 (Dense) (None, 2) 66 dense_4[0][0]
__________________________________________________________________________________________________
lambda_1 (Lambda) (None, 2) 0 dense_5[0][0]
dense_6[0][0]
__________________________________________________________________________________________________
functional_5 (Functional) (None, 28, 28, 1) 56385 lambda_1[0][0]
__________________________________________________________________________________________________
custom_variational_layer_1 (Cus (None, 28, 28, 1) 0 input_1[0][0]
functional_5[0][0]
==================================================================================================
Total params: 550,629
Trainable params: 550,629
Non-trainable params: 0
__________________________________________________________________________________________________
Train on 60000 samples, validate on 10000 samples
Epoch 1/10
60000/60000 [==============================] - 219s 4ms/sample - loss: 0.2173 - val_loss: 0.2016
...
Epoch 10/10
60000/60000 [==============================] - 234s 4ms/sample - loss: 0.1840 - val_loss: 0.1826
训练好模型,我们就可以使用 decoder 将任意潜在空间中的向量转换为图像。
1 | # 从潜在空间中采样一组点,解码为图像 |
书上到这里就结束了,并没有深入写之前提到的概念向量的应用😂,好遗憾啊。