这篇文章是我学习《Deep Learning with Python》(第二版,François Chollet 著) 时写的系列笔记之一。文章的内容是从 Jupyter notebooks 转成 Markdown 的,你可以去 GitHub 或 Gitee 找到原始的 .ipynb
笔记本。
你可以去这个网站在线阅读这本书的正版原文(英文)。这本书的作者也给出了配套的 Jupyter notebooks。
本文为 第8章 生成式深度学习 (Chapter 8. Generative deep learning) 的笔记之一。
用变分自编码器生成图像
前两篇介绍的 DeepDream 和 Neural Style Transfer 都只是有限地“修改”现有作品。而下面我们要介绍地 GAN 和 VAE 则是更加富有创造性的,这两种技术都是从图像的潜在空间中采样,并创建全新图像或编辑现有图像。
潜在空间(latent space)是一个向量空间,其中任意点都可以被映射为一张逼真的图像。而实现这种映射(潜在点->图像)的模块就是 GAN 的 generator,或者 VAE 的 decoder。
GAN、VAE 生成图像的关键就在于找到一个低维的「表示潜在空间」(latent space of representations)。一旦找到这样的潜在空间,从中采样,映射到图像空间,就可以生成全新的图像。
GAN 和 VAE 学习的潜在空间有很大的区别:
概念向量(concept vector):给定一个表示的潜在空间或一个嵌入空间,空间中的特定方向可能表示原始数据中有意义的变化轴。例如对于图像,人脸图像的潜在空间中可能存在一个代表「微笑」这个概念的向量(称为微笑向量,smile vector):对于代表某张人脸的潜在点 z,z+s 就是同一张人脸面带微笑的表示。
找到了这样的一些概念向量之后,我们就可以用这种方法来编辑图像了:将图像投射到潜在空间,和概念向量做运算来移动其表示,然后再解码到图像空间,就可以改变图像中的某一概念了——比如微笑程度:
自编码器是一种网络类型,接收一张图像,通过 encoder 模块将其映射到「潜在空间」,然后再通过 decoder 模块将其解码成与原始图像尺寸相同的输出。这东西训练时的目标是使输出和输入相同,所以我们把输入、输出用同一张图片。所以自编码器学习的是对原始输入进行重新构建。
通过对编码(编码器的输出)施加限制,可以让自编码器学到有用的数据潜在表示。比如限制编码要低维并且是稀疏的,这样编码器就可以将输入数据压缩为更少二进制位的信息:
变分自编码器 VAE,是一种现代化的自编码器。它是一种生成式模型,特别做利用概念向量进行图像编辑的任务。比起经典自编码器,VAE 可以学习更连续的、高度结构化的潜在空间。
VAE 不是将输入图像压缩成潜在空间中的固定编码,而是将图像转换为统计分布的参数——平均值和方差。VAE 解码的时候利用平均值和方差,从分布中随机采样一个元素,并将这个元素解码到原始输入。所以 VAE 的编码/解码过程是有一定的随机性的。
这个过程的随机性提高了 VAE 潜在空间的稳健性:VAE 需保证潜在空间采样的每个点都能解码为有效的输出,这迫使潜在空间的任何位置都对应有意义的表示。
上图展现了 VAE 的工作原理:
input_img
转换为表示潜在空间中的参数 z_mean
和 z_log_variance
;z = z_mean + exp(z_log_variance) * epsilon
,其中 epsilon 是取值很小的随机张量;epsilon 是随机的,所以需要与 input_img 编码的潜在位置(z-mean)靠近的每个点都能被解码为与 input_img 类似的图像,这个性质迫使潜在空间能够连续地有意义:潜在空间中任意两个相邻的点都会被解码为高度相似的图像。连续性以及潜在空间的低维度,又迫使潜在空间中的每个方向都表示数据中一个有意义的变化轴,这样就可以通过概念向量来进行操作。
用 Keras 实现 VAE 的伪代码如下:
z_mean, z_log_variance = encoder(input_img)
z = z_mean + exp(z_log_variance) * epsilon
reconstructed_img = decoder(z)
model = Model(input_img, reconstruced_img)
训练 VAE 需要两个损失函数:
我们具体实现编码器(encoder)网络:通过一个卷积神经网络,将输入图像 x 映射为两个向量 z_mean 和 z_log_var。
# 不使用及时执行模式
import tensorflow as tf
tf.compat.v1.disable_eager_execution()
# VAE 编码器网络
from tensorflow import keras
from tensorflow.keras import layers
from tensorflow.keras import backend as K
from tensorflow.keras.models import Model
import numpy as np
img_shape = (28, 28, 1)
batch_size = 16
latent_dim = 2 # 潜在空间的维度:2D平面
input_img = keras.Input(shape=img_shape)
x = layers.Conv2D(32, 3, padding='same', activation='relu')(input_img)
x = layers.Conv2D(64, 3, padding='same', activation='relu', strides=(2, 2))(x)
x = layers.Conv2D(64, 3, padding='same', activation='relu')(x)
x = layers.Conv2D(64, 3, padding='same', activation='relu')(x)
shape_before_flattening = K.int_shape(x)
x = layers.Flatten()(x)
x = layers.Dense(32, activation='relu')(x)
z_mean = layers.Dense(latent_dim)(x)
z_log_var = layers.Dense(latent_dim)(x)
接下来的代码将使用 z_mean 和 z_log_var 来生成(采样)一个潜在空间点 z。
# 潜在空间采样的函数
def sampling(args):
z_mean, z_log_var = args
epsilon = K.random_normal(shape=(K.shape(z_mean)[0], latent_dim),
mean=0.,
stddev=1.)
return z_mean + K.exp(z_log_var) * epsilon
z = layers.Lambda(sampling)([z_mean, z_log_var]) # 封装为层
然后是解码器的实现:将向量 z 的尺寸调整为图像大小,然后使用几个卷积层来得到最终的图像输出。
# VAE 解码器网络
decoder_input = layers.Input(K.int_shape(z)[1:])
x = layers.Dense(np.prod(shape_before_flattening[1:]),
activation='relu')(decoder_input)
x = layers.Reshape(shape_before_flattening[1:])(x)
x = layers.Conv2DTranspose(32, 3,
padding='same',
activation='relu',
strides=(2, 2))(x)
x = layers.Conv2D(1, 3,
padding='same',
activation='sigmoid')(x)
decoder = Model(decoder_input, x)
z_decoded = decoder(z)
VAE 要用两个损失,所以不能直接写成 loss(input, target)
,我们需要编写一个自定义层,在其中使用内置的 add_loss
方法来创建需要的损失。
# 用于计算 VAE 损失的自定义层
class CustomVariationalLayer(keras.layers.Layer):
def vae_loss(self, x, z_decoded):
x = K.flatten(x)
z_decoded = K.flatten(z_decoded)
xent_loss = keras.metrics.binary_crossentropy(x, z_decoded)
kl_loss = -5e-4 * K.mean(
1 + z_log_var - K.square(z_mean) - K.exp(z_log_var),
axis=-1)
return K.mean(xent_loss + kl_loss)
def call(self, inputs):
x = inputs[0]
z_decoded = inputs[1]
loss = self.vae_loss(x, z_decoded)
self.add_loss(loss, inputs=inputs)
return x
y = CustomVariationalLayer()([input_img, z_decoded])
最后,将模型实例化并开始训练。由于我们的损失以及包含在自定义层里面了,所以编译时无须指定外部损失(loss=None
),所以也就不需要外部指定的目标数据(y=None
)。
这里我们用 MNIST 去训练它,也就是生成手写数字的潜在空间。
from tensorflow.keras.datasets import mnist
vae = Model(input_img, y)
vae.compile(optimizer='rmsprop', loss=None)
vae.summary()
(x_train, _), (x_test, y_test) = mnist.load_data()
x_train = x_train.astype('float32') / 255.
x_train = x_train.reshape(x_train.shape + (1,))
x_test = x_test.astype('float32') / 255.
x_test = x_test.reshape(x_test.shape + (1,))
vae.fit(x=x_train, y=None,
shuffle=True,
epochs=10,
batch_size=batch_size,
validation_data=(x_test, None))
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 将任意潜在空间中的向量转换为图像。
# 从潜在空间中采样一组点,解码为图像
import matplotlib.pyplot as plt
from scipy.stats import norm
n = 15 # 显示 15x15个数
digit_size = 28
figure = np.zeros((digit_size * n, digit_size * n))
grid_x = norm.ppf(np.linspace(0.05, 0.95, n)) # ppf 函数对线性分隔的坐标进行变换,以生成潜在变量 z 的值
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_simple = np.array([[xi, yi]])
z_simple = np.tile(z_simple, batch_size).reshape(batch_size, 2)
x_decoded = decoder.predict(z_simple, batch_size=batch_size)
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()
书上到这里就结束了,并没有深入写之前提到的概念向量的应用,好遗憾啊。
By("CDFMLR", "2020-08-23");