假设机器学习是一个蛋糕,强化学习是蛋糕上的樱桃,监督学习是外面的糖衣,无监督学习则是蛋糕本体。—Yann LeCun
前面我们介绍了在给出样本及其的标签的情况下,神经网络如何学习的算法,这类算法需要学习的是在给定样本𝒙下的条件概率𝑃(𝑦|𝒙)。在社交网络蓬勃发展的今天,获取海量的样本数据𝒙,如照片、语音、文本等,是相对容易的,但困难的是获取这些数据所对应的标签信息,例如机器翻译,除了收集源语言的对话文本外,还需要待翻译的目标语言文本数据。
数据的标注工作目前主要还是依赖人的先验知识(Prior Knowledge)来完成,如亚马逊的 Mechanical Turk 系统专门负责数据标注业务,从全世界招纳兼职人员完成客户的数据标注任务。深度学习所需要的数据规模一般非常大,这种强依赖人工完成数据标注的方式代价较高,而且不可避免地引入标注人员的主观先验偏差。
面对海量的无标注数据,有没有办法能够从中学习到数据的分布𝑃(𝒙)的算法?这就是我们这章要介绍的无监督学习(Unsupervised Learning)算法。特别地,如果算法把𝒙作为监督信号来学习,这类算法称为自监督学习(Self-supervised Learning),本章要介绍的自编码器算法就是属于自监督学习范畴。
12.1 自编码器原理
让我们来考虑有监督学习中神经网络的功能:
o
=
f
θ
(
x
)
,
x
∈
R
d
i
n
,
o
∈
R
d
o
u
t
\boldsymbol{o}=f_{\theta}(\boldsymbol{x}), \boldsymbol{x} \in R^{d_{\mathrm{in}}}, \boldsymbol{o} \in R^{d_{\mathrm{out}}}
o=fθ(x),x∈Rdin,o∈Rdout
d
i
n
d_{in}
din是输入的特征向量长度,
d
o
u
t
d_{out}
dout是网络输出的向量长度。对于分类问题,网络模型通过把长度为
d
i
n
d_{in}
din输入特征向量𝒙变换到长度为
d
o
u
t
d_{out}
dout的输出向量𝒐,这个过程可以看成是特征降维的过程,把原始的高维输入向量𝒙变换到低维的变量𝒐。
特征降维(Dimensionality Reduction)在机器学习中有广泛的应用,比如文件压缩(Compression)、数据预处理(Preprocessing)等。最常见的降维算法有主成分分析法(Principal components analysis,简称 PCA),通过对协方差矩阵进行特征分解而得到数据的主要成分,但是 PCA 本质上是一种线性变换,提取特征的能力极为有限。
那么能不能利用神经网络的强大非线性表达能力去学习到低维的数据表示呢?问题的关键在于,训练神经网络一般需要一个显式的标签数据(或监督信号),但是无监督的数据没有额外的标注信息,只有数据𝒙本身。
于是,我们尝试着利用数据𝒙本身作为监督信号来指导网络的训练,即希望神经网络能够学习到映射𝑓𝜃: 𝒙 → 𝒙。我们把网络
f
θ
f_{\theta}
fθ切分为两个部分,前面的子网络尝试学习映射关系:
g
θ
1
:
x
→
z
g_{\theta_{1}}: x \rightarrow z
gθ1:x→z,后面的子网络尝试学习映射关系
h
θ
2
:
z
→
x
h_{\theta_{2}}: \mathbf{z} \rightarrow \boldsymbol{x}
hθ2:z→x,如图 12.1 所示。我们把
g
θ
1
g_{\theta_{1}}
gθ1看成一个数据编码(Encode)的过程,把高维度的输入𝒙编码成低维度的隐变量𝒛(LatentVariable,或隐藏变量),称为 Encoder 网络(编码器);
h
θ
2
h_{\theta_{2}}
hθ2看成数据解码(Decode)的过程,把编码过后的输入𝒛解码为高维度的𝒙,称为 Decoder 网络(解码器)。
编码器和解码器共同完成了输入数据𝒙的编码和解码过程,我们把整个网络模型
f
θ
f_{\theta}
fθ叫做自动编码器(Auto-Encoder),简称自编码器。如果使用深层神经网络来参数化
g
θ
1
g_{\theta_{1}}
gθ1和
h
θ
2
h_{\theta_{2}}
hθ2函数,则称为深度自编码器(Deep Auto-encoder),如图 12.2 所示。
自编码器能够将输入变换到隐藏向量𝒛,并通过解码器重建(Reconstruct,或恢复)出𝒙 。我们希望解码器的输出能够完美地或者近似恢复出原来的输入,即
x
‾
≈
x
\overline{\boldsymbol{x}} \approx \boldsymbol{x}
x≈x,那么,自编码器的优化目标可以写成:
其中
dist
(
x
,
x
ˉ
)
\operatorname{dist}(x, \bar{x})
dist(x,xˉ)表示 𝒙和
x
ˉ
\bar{x}
xˉ 的距离度量,称为重建误差函数。最常见的度量方法有欧氏距离(Euclidean distance)的平方,计算方法如下:
L
=
∑
i
(
x
i
−
x
ˉ
i
)
2
\mathcal{L}=\sum_{i}\left(x_{i}-\bar{x}_{i}\right)^{2}
L=i∑(xi−xˉi)2
它和均方误差原理上是等价的。自编码器网络和普通的神经网络并没有本质的区别,只不过训练的监督信号由标签𝒚变成了自身𝒙。借助于深层神经网络的非线性特征提取能力,自编码器可以获得良好的数据表示,相对于 PCA 等线性方法,自编码器性能更加优秀,甚至可以更加完美的恢复出输入𝒙。
在图 12.3(a)中,第 1 行是随机采样自测试集的真实 MNIST 手写数字图片,第 2、3、4 行分别是基于长度为 30 的隐藏向量,使用自编码器、Logistic PCA 和标准 PCA 算法恢复出的重建样本图片;在图 12.3(b)中,第 1 行为真实的人像图片,第 2、3 行分别是基于长度为 30 的隐藏向量,使用自编码器和标准 PCA 算法恢复出的重建样本。可以看到,使用深层神经网络的自编码器重建出图片相对清晰,还原度较高,而 PCA 算法重建出的图片较模糊。
12.2 Fashion MNIST 图片重建实战
自编码器算法原理非常简单,实现方便,训练也较稳定,相对于 PCA 算法,神经网络的强大表达能力可以学习到输入的高层抽象的隐藏特征向量𝒛,同时也能够基于𝒛重建出输入。这里我们基于 Fashsion MNIST 数据集进行图片重建实战。
12.2.1 Fashion MNIST 数据集
Fashion MNIST 是一个定位在比 MNIST 图片识别问题稍复杂的数据集,它的设定与MNIST 几乎完全一样,包含了 10 类不同类型的衣服、鞋子、包等灰度图片,图片大小为28 × 28,共 70000 张图片,其中 60000 张用于训练集,10000 张用于测试集,如图 12.4所示,每行是一种类别图片。可以看到,Fashion MNIST 除了图片内容与 MNIST 不一样,其它设定都相同,大部分情况可以直接替换掉原来基于 MNIST 训练的算法代码,而不需要额外修改。由于 Fashion MNIST 图片识别相对于 MNIST 图片更难,因此可以用于测试稍复杂的算法性能。
在 TensorFlow 中,加载 Fashion MNIST 数据集同样非常方便,利用keras.datasets.fashion_mnist.load_data()
函数即可在线下载、管理和加载。代码如下:
from tensorflow import keras
import tensorflow as tf
import numpy as np
batchsz = 512
#加载Fashion MNIST图片数据集
(x_train,y_train),(x_test,y_test)=keras.datasets.fashion_mnist.load_data()
#归一化
x_train,x_test=x_train.astype(np.float32)/255.,x_test.astype(np.float32)/255.
#只需要通过图片数据即可构建数据集对象,不需要标签
train_db=tf.data.Dataset.from_tensor_slices(x_train)
train_db=train_db.shuffle(batchsz*5).batch(batchsz)
#构建测试集对象
test_db=tf.data.Dataset.from_tensor_slices(x_test)
test_db=test_db.batch(batchsz)
12.2.2 编码器
我们利用编码器将输入图片
x
∈
R
784
x \in R^{784}
x∈R784降维到较低维度的隐藏向量:
h
∈
R
20
\boldsymbol{h} \in R^{20}
h∈R20,并基于隐藏向量
h
h
h利用解码器重建图片,自编码器模型如图 12.5 所示,编码器由 3 层全连接层网络组成,输出节点数分别为 256、128、20,解码器同样由 3 层全连接网络组成,输出节点数分别为 128、256、784。
首先是编码器子网络的实现。利用 3 层的神经网络将长度为 784 的图片向量数据依次降维到 256、128,最后降维到 h_dim 维度,每层使用 ReLU 激活函数,最后一层不使用激活函数。代码如下:
#创建Encoders网络,实现在自编码器类的初始化函数中
h_dim = 20
Sequential([
layers.Dense(256,activation=tf.nn.relu),
layers.Dense(128,activation=tf.nn.relu),
layers.Dense(h_dim)
])
12.2.3 解码器
然后再来创建解码器子网络,这里基于隐藏向量 h_dim 依次升维到 128、256、784 长度,除最后一层,激活函数使用 ReLU 函数。解码器的输出为 784 长度的向量,代表了打平后的28 × 28大小图片,通过 Reshape 操作即可恢复为图片矩阵。代码如下:
# 创建Decoders网络
self.decoder=Sequential([
layers.Dense(128,activation=tf.nn.relu),
layers.Dense(256,activation=tf.nn.relu),
layers.Dense(784)
])
12.2.4 自编码器
上述的编码器和解码器 2 个子网络均实现在自编码器类 AE 中,我们在初始化函数中同时创建这两个子网络。代码如下:
class AE(keras.Model):
# 自编码器模型类,包含了Encoder和Decoder2个子网络
def __init__(self):
super(AE,self).__init__()
#创建Encoders网络
self.encoder=Sequential([
layers.Dense(256,activation=tf.nn.relu),
layers.Dense(128,activation=tf.nn.relu),
layers.Dense(h_dim)
])
#创建Decoders网络
self.decoder=Sequential([
layers.Dense(128,activation=tf.nn.relu),
layers.Dense(256,activation=tf.nn.relu),
layers.Dense(784)
])
接下来将前向传播过程实现在 call 函数中,输入图片首先通过 encoder 子网络得到隐藏向量 h,再通过 decoder 得到重建图片。依次调用编码器和解码器的前向传播函数即可,代码如下:
def call(self,inputs,training=None):
# 前向传播函数
# 编码获得隐藏向量h,[b,784]=>[b,20]
h=self.encoder(inputs)
#解码获得重建图片,[b,20]=>[b,784]
x_hat=self.decoder(h)
return x_hat
12.2.5 网络训练
自编码器的训练过程与分类器的基本一致,通过误差函数计算出重建向量 X ‾ \overline{\boldsymbol{X}} X与原始输入向量𝒙之间的距离,再利用 TensorFlow 的自动求导机制同时求出 encoder 和 decoder 的梯度,循环更新即可。
首先创建自编码器实例和优化器,并设置合适的学习率。例如:
#创建网络对象
model=AE()
# 指定输入大小
model.build(input_shape=(4,784))
#打印网络信息
model.summary()
optimizer=optimizers.Adam(lr=lr)
这里固定训练 100 个 Epoch,每次通过前向计算获得重建图片向量,并利用tf.nn.sigmoid_cross_entropy_with_logits
损失函数计算重建图片与原始图片直接的误差,实际上利用 MSE 误差函数也是可行的。代码如下:
for epoch in range(100):#训练100个Epoch
for step,x in enumerate(train_db):
# 打平,[b,28,28]=>[b,784]
x=tf.reshape(x,[-1,784])
# 构建梯度计算器
with tf.GradientTape()as tape:
#前向计算获得重建的图片
x_rec_logits=model(x)
#计算重见图片与输入之间的损失函数
rec_loss=tf.nn.sigmoid_cross_entropy_with_logits(labels=x,logits=x_rec_logits)
#计算均值
rec_loss=tf.reduce_mean(rec_loss)
#自动求导,包含了2个子网络的梯度
grades=tape.gradient(rec_loss,model.trainable_varibales)
#自动更新,同时更新2个子网络
optimizer.apply_gradients(zip(grades,model.trainable_varibales))
if step%100==0:
#间隔性打印训练误差
print(epoch,step,float(rec_loss))
12.2.6 图片重建
与分类问题不同的是,自编码器的模型性能一般不好量化评价,尽管ℒ值可以在一定程度上代表网络的学习效果,但我们最终希望获得还原度较高、样式较丰富的重建样本。因此一般需要根据具体问题来讨论自编码器的学习效果,比如对于图片重建,一般依赖于人工主观评价图片生成的质量,或利用某些图片逼真度计算方法(如 Inception Score 和Frechet Inception Distance)来辅助评估。
为了测试图片重建效果,我们把数据集切分为训练集与测试集,其中测试集不参与训。我们从测试集中随机采样测试图片 x ∈ D test x \in \mathbb{D}^{\text {test }} x∈Dtest ,经过自编码器计算得到重建后的图片,然后将真实图片与重建图片保存为图片阵列,并可视化,方便比对。代码如下:
#重建图片,从测试集采样一匹图片
x=next(iter(test_db))
logits=model(tf.reshape(x,[1,784]))#打平送入自编码器
x_hat=tf.sigmoid(logits)#将输出转换为像素值,使用sigmoid函数
#恢复为28*28,[b,784]=>[b,28,28]
x_hat=tf.reshape(x_hat,[-1,28,28])
#输入的50张+重构的前50张图片合并,[b,28,28]=>[2b,28,28]
x_concat=tf.constant([x[:50],x_hat[:50]],axis=0)
x_concat=x_concat.numpy()*255. #恢复为0~255范围
x_concat=x_concat.astype(np.uint8)#转换为整型
save_images(x_concat,'ae_images/rec_epoch_%d.png'%epoch)#保存图片
图片重建的效果如图 12.6、图 12.7、图 12.8 所示,其中每张图片的左边 5 列为真实图片,右边 5 列为对应的重建图片。可以看到,第一个 Epoch 时,图片重建效果较差,图片非常模糊,逼真度较差;随着训练的进行,重建图片边缘越来越清晰,第 100 个 Epoch时,重建的图片效果已经比较接近真实图片。
这里的 save_images 函数负责将多张图片合并并保存为一张大图,这部分代码使用 PIL图片库完成图片阵列逻辑,代码如下:
from PIL import Image
def save_images(imgs,name):
#创建280*280大小图片阵列
new_im=Image.new('L',(280,280))
index=0
for i in range(0,280,28):
for j in range(0,280,28):
im=imgs[index]
im=Image.fromarray(im,mode='L')
new_im.paste(im,(i,j))#写入对应位置
index+=1
#保存图片阵列
new_im.save(name)
我们接下来总结以下上面的代码:
#!/usr/bin/env python
# encoding: utf-8
import os
import numpy as np
import tensorflow as tf
from PIL import Image
from tensorflow import keras
from tensorflow.keras import Sequential, layers
tf.random.set_seed(22)
np.random.seed(22)
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
def save_images(imgs, name):
# 创建 280x280 大小图片阵列
new_im = Image.new('L', (280, 280))
index = 0
# 10 行图片阵列
for i in range(0, 280, 28):
# 10 列图片阵列
for j in range(0, 280, 28):
im = imgs[index]
im = Image.fromarray(im, mode='L')
# 写入对应位置
new_im.paste(im, (i, j))
index += 1
# 保存图片阵列
new_im.save(name)
def load_dataset(batchsz):
# 加载 Fashion MNIST 图片数据集
(x_train, y_train), (x_test, y_test) = keras.datasets.fashion_mnist.load_data()
# 归一化
x_train, x_test = x_train.astype(np.float32) / 255., x_test.astype(np.float32) / 255.
# 只需要通过图片数据即可构建数据集对象,不需要标签
train_db = tf.data.Dataset.from_tensor_slices(x_train)
train_db = train_db.shuffle(batchsz * 5).batch(batchsz)
# 构建测试集对象
test_db = tf.data.Dataset.from_tensor_slices(x_test)
test_db = test_db.batch(batchsz)
return train_db, test_db
class AE(keras.Model):
# 自编码器模型类,包含了 Encoder 和 Decoder2 个子网络
def __init__(self, h_dim):
super(AE, self).__init__()
# 创建 Encoders 网络,实现在自编码器类的初始化函数中
self.encoder = Sequential([
layers.Dense(256, activation=tf.nn.relu),
layers.Dense(128, activation=tf.nn.relu),
layers.Dense(h_dim)
])
# 创建 Decoders 网络
self.decoder = Sequential([
layers.Dense(128, activation=tf.nn.relu),
layers.Dense(256, activation=tf.nn.relu),
layers.Dense(784)
])
def call(self, inputs, training=None):
# 前向传播函数
# 编码获得隐藏向量 h,[b, 784] => [b, 20]
h = self.encoder(inputs)
# 解码获得重建图片, [b, 20] => [b, 784]
x_hat = self.decoder(h)
return x_hat
def build_model(h_dim):
# 创建网络对象
model = AE(h_dim)
# 指定输入大小
model.build(input_shape=(None, 784))
# 打印网络信息
model.summary()
return model
def train(train_db, model, optimizer, epoch):
# 遍历训练集
for step, x in enumerate(train_db):
# [b, 28, 28] => [b, 784]
# 打平, [b, 28, 28] => [b, 784]
x = tf.reshape(x, [-1, 784])
# 构建梯度记录器
with tf.GradientTape() as tape:
# 前向计算获得重建的图片
x_rec_logits = model(x)
# 计算重建图片与输入之间的损失函数
rec_loss = tf.losses.binary_crossentropy(x, x_rec_logits, from_logits=True)
# 计算均值
rec_loss = tf.reduce_mean(rec_loss)
# 自动求导,包含了2个子网络的梯度
grads = tape.gradient(rec_loss, model.trainable_variables)
# 自动更新,同时更新2个子网络
optimizer.apply_gradients(zip(grads, model.trainable_variables))
if step % 100 == 0:
# 间隔性打印训练误差
print(epoch, step, float(rec_loss))
return model
def evaluation(test_db, model, epoch):
# evaluation
# 重建图片,从测试集采样一批图片
x = next(iter(test_db))
# 打平并送入自编码器
logits = model(tf.reshape(x, [-1, 784]))
# 将输出转换为像素值,使用sigmoid函数
x_hat = tf.sigmoid(logits)
# 恢复为 28x28,[b, 784] => [b, 28, 28]
x_hat = tf.reshape(x_hat, [-1, 28, 28])
# 输入的前50张+重建的前50张图片合并, [b, 28, 28] => [2b, 28, 28]
x_concat = tf.concat([x[:50], x_hat[:50]], axis=0)
# 恢复为0~255范围
x_concat = x_concat.numpy() * 255.
# 转换为整型
x_concat = x_concat.astype(np.uint8)
# 保存图片
save_images(x_concat, './rec_epoch_%d.png' % epoch)
def main():
h_dim = 20
batchsz = 512
lr = 1e-3
train_db, test_db = load_dataset(batchsz)
model = build_model(h_dim)
# 创建优化器,并设置学习率
optimizer = tf.optimizers.Adam(lr=lr)
# 训练100个Epoch
for epoch in range(100):
model = train(train_db, model, optimizer, epoch)
evaluation(test_db, model, epoch)
if __name__ == '__main__':
main()
经过100个epoch的训练,我们得到了每次训练的图片比较。
12.3 自编码器变种
一般而言,自编码器网络的训练较为稳定,但是由于损失函数是直接度量重建样本与真实样本的底层特征之间的距离,而不是评价重建样本的逼真度和多样性等抽象指标,因此在某些任务上效果一般,如图片重建,容易出现重建图片边缘模糊,逼真度相对真实图片仍有不小差距。为了尝试让自编码器学习到数据的真实分布,产生了一系列的自编码器变种网络。下面将介绍几种典型的自编码器变种模型。
12.3.1 Denoising Auto-Encoder
为了防止神经网络记忆住输入数据的底层特征,Denoising Auto-Encoders 给输入数据添加随机的噪声扰动,如给输入𝒙添加采样自高斯分布的噪声𝜀:
x
~
=
x
+
ε
,
ε
∼
N
(
0
,
var
)
\tilde{x}=x+\varepsilon, \varepsilon \sim \mathcal{N}(0, \text { var })
x~=x+ε,ε∼N(0, var )
添加噪声后,网络需要从𝒙̃学习到数据的真实隐藏变量 z,并还原出原始的输入𝒙,如图12.9 所示。模型的优化目标为:
θ
∗
=
argmin
θ
dist
(
h
θ
2
(
g
θ
1
(
x
~
)
)
,
x
)
⏟
\theta^{*}=\underbrace{\underset{\theta}{\operatorname{argmin}} \operatorname{dist}\left(h_{\theta_{2}}\left(g_{\theta_{1}}(\widetilde{x})\right), x\right)}
θ∗=
θargmindist(hθ2(gθ1(x
)),x)
12.3.2 Dropout Auto-Encoder
自编码器网络同样面临过拟合的风险,Dropout Auto-Encoder 通过随机断开网络的连接来减少网络的表达能力,防止过拟合。Dropout Auto-Encoder
实现非常简单,通过在网络层中插入 Dropout 层即可实现网络连接的随机断开。
12.3.3 Adversarial Auto-Encoder
为了能够方便地从某个已知的先验分布中𝑝(𝒛)采样隐藏变量𝒛,方便利用𝑝(𝒛)来重建输入,对抗自编码器(Adversarial Auto-Encoder)利用额外的判别器网络(Discriminator,简称 D网络)来判定降维的隐藏变量𝒛是否采样自先验分布𝑝(𝒛),如图 12.10 所示。判别器网络的输出为一个属于[0,1]区间的变量,表征隐藏向量是否采样自先验分布𝑝(𝒛):所有采样自先验分布𝑝(𝒛)的𝒛标注为真,采样自编码器的条件概率𝑞(𝒛|𝒙)的𝒛标注为假。通过这种方式训练,除了可以重建样本,还可以约束条件概率分布𝑞(𝒛|𝒙)逼近先验分布𝑝(𝒛)。
对抗自编码器是从下一章要介绍的生成对抗网络算法衍生而来,在学习完对抗生成网络后可以加深对对抗自编码器的理解。
12.4 变分自编码器
基本的自编码器本质上是学习输入𝒙和隐藏变量𝒛之间映射关系,它是一个判别模型(Discriminative model),并不是生成模型(Generative model)。那么能不能将自编码器调整为生成模型,方便地生成样本呢?
给定隐藏变量的分布P(𝒛),如果可以学习到条件概率分布P(𝒙|𝒛),则通过对联合概率分布P(𝒙, 𝒛) = P(𝒙|𝒛)P(𝒛)进行采样,生成不同的样本。变分自编码器(Variational AutoEncoders,简称 VAE)就可以实此目的,如图 12.11 所示。如果从神经网络的角度来理解的话,VAE 和前面的自编码器一样,非常直观好理解;但是 VAE 的理论推导稍复杂,接下来我们先从神经网络的角度去阐述 VAE,再从概率角度去推导 VAE。
从神经网络的角度来看,VAE 相对于自编码器模型,同样具有编码器和解码器两个子网络。解码器接受输入𝒙,输出为隐变量𝒛;解码器负责将隐变量𝒛解码为重建的𝒙 。不同的是,VAE 模型对隐变量𝒛的分布有显式地约束,希望隐变量𝒛符合预设的先验分布P(𝒛)。因此,在损失函数的设计上,除了原有的重建误差项外,还添加了隐变量𝒛分布的约束项。
12.4.1 VAE 原理
从概率的角度,我们假设任何数据集都采样自某个分布𝑝(𝒙|𝒛),𝒛是隐藏变量,代表了某种内部特征,比如手写数字的图片𝒙,𝒛可以表示字体的大小、书写风格、加粗、斜体等设定,它符合某个先验分布𝑝(𝒛),在给定具体隐藏变量𝒛的情况下,我们可以从学到了分布𝑝(𝒙|𝒛)中采样一系列的生成样本,这些样本都具有𝒛所表示的共性。
通常可以假设𝑝(𝒛)符合已知的分布,比如𝒩(0,1)。在𝑝(𝒛)已知的条件下,我们的目的就是希望能学会生成概率模型𝑝(𝒙|𝒛)。这里可以采用最大似然估计(Maximum LikelihoodEstimation)方法:一个好的模型,应该拥有很大的概率生成真实的样本𝒙 ∈ 𝔻。如果我们的生成模型𝑝(𝒙|𝒛)是用𝜃来参数化,那么我们的神经网络的优化目标是:
max
θ
p
(
x
)
=
∫
z
p
(
x
∣
z
)
p
(
z
)
d
z
\max _{\theta} p(\boldsymbol{x})=\int_{\mathbf{z}} p(\boldsymbol{x} | \mathbf{z}) p(\mathbf{z}) d \mathbf{z}
θmaxp(x)=∫zp(x∣z)p(z)dz
很遗憾的是,由于𝒛是连续变量,上述积分没法转换为离散形式,导致很难直接优化。
换一个思路,利用变分推断(Variational Inference)的思想,我们通过分布
q
ϕ
(
z
∣
x
)
q_{\phi}(\mathbf{z} | \boldsymbol{x})
qϕ(z∣x)来逼近𝑝(𝒛|𝒙),即需要最小化
q
ϕ
(
z
∣
x
)
q_{\phi}(\mathbf{z} | \boldsymbol{x})
qϕ(z∣x)与𝑝(𝒛|𝒙)之间的距离:
min
ϕ
D
K
L
(
q
ϕ
(
z
∣
x
)
∥
p
(
z
∣
x
)
)
\min _{\phi} \mathbb{D}_{K L}\left(q_{\phi}(\mathbf{z} | \boldsymbol{x}) \| p(\mathbf{z} | \boldsymbol{x})\right)
ϕminDKL(qϕ(z∣x)∥p(z∣x))
其中 KL 散度𝔻𝐾𝐿是一种衡量分布𝑞和𝑝之间的差距的度量,定义为:
D
K
L
(
q
∥
p
)
=
∫
x
q
(
x
)
log
q
(
x
)
p
(
x
)
d
x
\mathbb{D}_{K L}(q \| p)=\int_{x} q(x) \log \frac{q(x)}{p(x)} d x
DKL(q∥p)=∫xq(x)logp(x)q(x)dx
严格地说,距离一般是对称的,而 KL 散度并不对称。将 KL 散度展开为
D
K
L
(
q
ϕ
(
z
∣
x
)
∥
p
(
z
∣
x
)
)
=
∫
z
q
ϕ
(
z
∣
x
)
log
q
ϕ
(
z
∣
x
)
p
(
z
∣
x
)
d
z
\mathbb{D}_{K L}\left(q_{\phi}(\mathbf{z} | \boldsymbol{x}) \| p(\mathbf{z} | \boldsymbol{x})\right)=\int_{\mathbf{z}} q_{\phi}(\mathbf{z} | \boldsymbol{x}) \log \frac{q_{\phi}(\mathbf{z} | \boldsymbol{x})}{p(\mathbf{z} | \boldsymbol{x})} d \mathbf{z}
DKL(qϕ(z∣x)∥p(z∣x))=∫zqϕ(z∣x)logp(z∣x)qϕ(z∣x)dz
利用性质
p
(
z
∣
x
)
⋅
p
(
x
)
=
p
(
x
,
z
)
p(\mathbf{z} | \boldsymbol{x}) \cdot p(\boldsymbol{x})=p(\boldsymbol{x}, \mathbf{z})
p(z∣x)⋅p(x)=p(x,z)
可以得到
D
K
L
(
q
ϕ
(
z
∣
x
)
∥
p
(
z
∣
x
)
)
=
∫
z
q
ϕ
(
z
∣
x
)
log
q
ϕ
(
z
∣
x
)
p
(
x
)
p
(
x
,
z
)
d
z
=
∫
z
q
ϕ
(
z
∣
x
)
log
q
ϕ
(
z
∣
x
)
p
(
x
,
z
)
d
z
+
∫
z
q
ϕ
(
z
∣
x
)
log
p
(
x
)
d
z
\begin{array}{l} \mathbb{D}_{K L}\left(q_{\phi}(\mathbf{z} | \boldsymbol{x}) \| p(\mathbf{z} | \boldsymbol{x})\right)=\int_{\mathbf{z}} q_{\phi}(\mathbf{z} | \boldsymbol{x}) \log \frac{q_{\phi}(\mathbf{z} | \boldsymbol{x}) p(\boldsymbol{x})}{p(\boldsymbol{x}, \mathbf{z})} d \mathbf{z} \\ =\int_{\mathbf{z}} q_{\phi}(\mathbf{z} | \boldsymbol{x}) \log \frac{q_{\phi}(\mathbf{z} | \boldsymbol{x})}{p(\boldsymbol{x}, \mathbf{z})} d z+\int_{\mathbf{z}} q_{\phi}(\mathbf{z} | \boldsymbol{x}) \log p(\boldsymbol{x}) d \mathbf{z} \end{array}
DKL(qϕ(z∣x)∥p(z∣x))=∫zqϕ(z∣x)logp(x,z)qϕ(z∣x)p(x)dz=∫zqϕ(z∣x)logp(x,z)qϕ(z∣x)dz+∫zqϕ(z∣x)logp(x)dz
=
−
(
−
∫
z
q
ϕ
(
z
∣
x
)
log
q
ϕ
(
z
∣
x
)
p
(
x
,
z
)
d
z
)
⏟
L
(
ϕ
,
θ
)
+
log
p
(
x
)
=-\underbrace{\left(-\int_{z} q_{\phi}(\mathbf{z} | \boldsymbol{x}) \log \frac{q_{\phi}(\mathbf{z} | \boldsymbol{x})}{p(\boldsymbol{x}, \mathbf{z})} d z\right)}_{\mathcal{L}(\phi, \boldsymbol{\theta})}+\log p(\boldsymbol{x})
=−L(ϕ,θ)
(−∫zqϕ(z∣x)logp(x,z)qϕ(z∣x)dz)+logp(x)
我们将
−
∫
z
q
ϕ
(
z
∣
x
)
log
q
ϕ
(
Z
∣
x
)
p
(
x
,
z
)
d
z
-\int_{\mathbf{z}} q_{\phi}(\mathbf{z} | \boldsymbol{x}) \log \frac{q_{\phi}(\mathbf{Z} | \boldsymbol{x})}{p(\boldsymbol{x}, \mathbf{z})} d z
−∫zqϕ(z∣x)logp(x,z)qϕ(Z∣x)dz定义为
L
(
ϕ
,
θ
)
\mathcal{L}(\phi, \theta)
L(ϕ,θ)项,上式即为
D
K
L
(
q
ϕ
(
z
∣
x
)
∥
p
(
z
∣
x
)
)
=
−
L
(
ϕ
,
θ
)
+
log
p
(
x
)
\mathbb{D}_{K L}\left(q_{\phi}(\mathbf{z} | \boldsymbol{x}) \| p(\mathbf{z} | \boldsymbol{x})\right)=-\mathcal{L}(\phi, \theta)+\log p(\boldsymbol{x})
DKL(qϕ(z∣x)∥p(z∣x))=−L(ϕ,θ)+logp(x)
其中
L
(
ϕ
,
θ
)
=
−
∫
z
q
ϕ
(
z
∣
x
)
log
q
ϕ
(
z
∣
x
)
p
(
x
,
z
)
d
z
\mathcal{L}(\phi, \theta)=-\int_{\mathrm{z}} q_{\phi}(\mathbf{z} | \boldsymbol{x}) \log \frac{q_{\phi}(\mathbf{z} | \boldsymbol{x})}{p(\boldsymbol{x}, \mathbf{z})} d z
L(ϕ,θ)=−∫zqϕ(z∣x)logp(x,z)qϕ(z∣x)dz
考虑到
D
K
L
(
q
ϕ
(
z
∣
x
)
∥
p
(
z
∣
x
)
)
≥
0
\mathbb{D}_{K L}\left(q_{\phi}(\mathbf{z} | \boldsymbol{x}) \| p(\mathbf{z} | \boldsymbol{x})\right) \geq 0
DKL(qϕ(z∣x)∥p(z∣x))≥0
因此
L
(
ϕ
,
θ
)
≤
log
p
(
x
)
\mathcal{L}(\phi, \theta) \leq \log p(x)
L(ϕ,θ)≤logp(x)
也就是说,
L
(
ϕ
,
θ
)
\mathcal{L}(\phi, \theta)
L(ϕ,θ)是
log
p
(
x
)
\log p(x)
logp(x)的下界限(Lower Bound),优化目标
L
(
ϕ
,
θ
)
\mathcal{L}(\phi, \theta)
L(ϕ,θ)称为 EvidenceLower Bound Objective(ELBO)。我们是目标是最大化似然概率𝑝(𝒙),或最大化log 𝑝 (𝑥),那么可以通过最大化其下界限
L
(
ϕ
,
θ
)
\mathcal{L}(\phi, \theta)
L(ϕ,θ)实现。
现在我们来分析如何最大化ℒ(𝜙, 𝜃)函数,展开可得:
L
(
θ
,
ϕ
)
=
∫
z
q
ϕ
(
z
∣
x
)
log
p
θ
(
x
,
z
)
q
ϕ
(
z
∣
x
)
=
∫
z
q
ϕ
(
z
∣
x
)
log
p
(
z
)
p
θ
(
x
∣
z
)
q
ϕ
(
z
∣
x
)
=
∫
z
q
ϕ
(
z
∣
x
)
log
p
(
z
)
q
ϕ
(
z
∣
x
)
+
∫
z
q
ϕ
(
z
∣
x
)
log
p
θ
(
x
∣
z
)
=
−
∫
z
q
ϕ
(
z
∣
x
)
log
q
ϕ
(
z
∣
x
)
p
(
z
)
+
E
z
∼
q
[
log
p
θ
(
x
∣
z
)
]
=
−
D
K
L
(
q
ϕ
(
z
∣
x
)
∥
p
(
z
)
)
+
E
z
∼
q
[
log
p
θ
(
x
∣
z
)
]
\begin{array}{c} \mathcal{L}(\theta, \phi)=\int_{\mathbf{z}} q_{\phi}(\mathbf{z} | \boldsymbol{x}) \log \frac{p_{\theta}(\boldsymbol{x}, \mathbf{z})}{q_{\phi}(\mathbf{z} | \boldsymbol{x})} \\ =\int_{\mathbf{z}} q_{\phi}(\mathbf{z} | \boldsymbol{x}) \log \frac{p(\mathbf{z}) p_{\theta}(\boldsymbol{x} | \mathbf{z})}{q_{\phi}(\mathbf{z} | \boldsymbol{x})} \\ =\int_{\mathbf{z}} q_{\phi}(\mathbf{z} | \boldsymbol{x}) \log \frac{p(\mathbf{z})}{q_{\phi}(\mathbf{z} | \boldsymbol{x})}+\int_{\mathbf{z}} q_{\phi}(\mathbf{z} | \boldsymbol{x}) \log p_{\theta}(\boldsymbol{x} | \mathbf{z}) \\ =-\int_{z} q_{\phi}(\mathbf{z} | \boldsymbol{x}) \log \frac{q_{\phi}(\mathbf{z} | \boldsymbol{x})}{p(\mathbf{z})}+\mathbb{E}_{\mathbf{z} \sim q}\left[\log p_{\theta}(\boldsymbol{x} | \mathbf{z})\right] \\ =-\mathbb{D}_{K L}\left(q_{\phi}(\mathbf{z} | \boldsymbol{x}) \| p(\mathbf{z})\right)+\mathbb{E}_{\mathbf{z} \sim q}\left[\log p_{\theta}(\boldsymbol{x} | \mathbf{z})\right] \end{array}
L(θ,ϕ)=∫zqϕ(z∣x)logqϕ(z∣x)pθ(x,z)=∫zqϕ(z∣x)logqϕ(z∣x)p(z)pθ(x∣z)=∫zqϕ(z∣x)logqϕ(z∣x)p(z)+∫zqϕ(z∣x)logpθ(x∣z)=−∫zqϕ(z∣x)logp(z)qϕ(z∣x)+Ez∼q[logpθ(x∣z)]=−DKL(qϕ(z∣x)∥p(z))+Ez∼q[logpθ(x∣z)]
因此
L
(
θ
,
ϕ
)
=
−
D
K
L
(
q
ϕ
(
z
∣
x
)
∥
p
(
z
)
)
+
E
z
∼
q
[
log
p
θ
(
x
∣
z
)
]
\mathcal{L}(\theta, \phi)=-\mathbb{D}_{K L}\left(q_{\phi}(\mathbf{z} | \boldsymbol{x}) \| p(\mathbf{z})\right)+\mathbb{E}_{\mathbf{z} \sim q}\left[\log p_{\theta}(\boldsymbol{x} | \mathbf{z})\right]
L(θ,ϕ)=−DKL(qϕ(z∣x)∥p(z))+Ez∼q[logpθ(x∣z)]
可以用编码器网络参数化
q
ϕ
(
z
∣
x
)
q_{\phi}(\mathbf{z} | \boldsymbol{x})
qϕ(z∣x)函数,解码器网络参数化𝑝𝜃(𝒙|𝒛)函数,通过计算解码器的输出分布
q
ϕ
(
z
∣
x
)
q_{\phi}(\mathbf{z} | \boldsymbol{x})
qϕ(z∣x)与先验分布𝑝(𝒛)之间的 KL 散度,以及解码器的似然概率
log
p
θ
(
x
∣
z
)
\log p_{\theta}(x | z)
logpθ(x∣z)构成的损失函数,即可优化ℒ(𝜃,𝜙)目标。
特别地,当
q
ϕ
(
z
∣
x
)
q_{\phi}(\mathbf{z} | \boldsymbol{x})
qϕ(z∣x)和𝑝(z)都假设为正态分布时,
D
K
L
(
q
ϕ
(
z
∣
x
)
∥
p
(
z
)
)
\mathbb{D}_{K L}\left(q_{\phi}(z | x) \| p(z)\right)
DKL(qϕ(z∣x)∥p(z))计算可以简化为:
D
K
L
(
q
ϕ
(
z
∣
x
)
∥
p
(
z
)
)
=
log
σ
2
σ
1
+
σ
1
2
+
(
μ
1
−
μ
2
)
2
2
σ
2
2
−
1
2
\mathbb{D}_{K L}\left(q_{\phi}(z | x) \| p(z)\right)=\log \frac{\sigma_{2}}{\sigma_{1}}+\frac{\sigma_{1}^{2}+\left(\mu_{1}-\mu_{2}\right)^{2}}{2 \sigma_{2}^{2}}-\frac{1}{2}
DKL(qϕ(z∣x)∥p(z))=logσ1σ2+2σ22σ12+(μ1−μ2)2−21
更特别地,当
q
ϕ
(
z
∣
x
)
q_{\phi}(\mathbf{z} | \boldsymbol{x})
qϕ(z∣x)为正态分布𝒩(𝜇1, 𝜎1),𝑝( )为正态分布𝒩(0,1)时,即𝜇2 = 0, 𝜎2 =1,此时
D
K
L
(
q
ϕ
(
z
∣
x
)
∥
p
(
z
)
)
=
−
log
σ
1
+
0.5
σ
1
2
+
0.5
μ
1
2
−
0.5
\mathbb{D}_{K L}\left(q_{\phi}(z | x) \| p(z)\right)=-\log \sigma_{1}+0.5 \sigma_{1}^{2}+0.5 \mu_{1}^{2}-0.5
DKL(qϕ(z∣x)∥p(z))=−logσ1+0.5σ12+0.5μ12−0.5
上述过程将ℒ(𝜃, 𝜙)表达式中的
D
K
L
(
q
ϕ
(
z
∣
x
)
∥
p
(
z
)
)
\mathbb{D}_{K L}\left(q_{\phi}(\mathbf{z} | \boldsymbol{x}) \| p(\mathbf{z})\right)
DKL(qϕ(z∣x)∥p(z))项变得更易于计算,而
E
z
∼
q
[
log
p
θ
(
x
∣
z
)
]
\mathbb{E}_{\mathbf{z} \sim q}\left[\log p_{\theta}(\boldsymbol{x} | \mathbf{z})\right]
Ez∼q[logpθ(x∣z)]同样可以基于自编码器中的重建误差函数实现。
因此,VAE 模型的优化目标由最大化ℒ(𝜙, 𝜃)函数转换为:
min
D
K
L
(
q
ϕ
(
z
∣
x
)
∥
p
(
z
)
)
\min \mathbb{D}_{K L}\left(q_{\phi}(\mathbf{z} | \boldsymbol{x}) \| p(\mathbf{z})\right)
minDKL(qϕ(z∣x)∥p(z))
和
max
E
z
∼
q
[
log
p
θ
(
x
∣
z
)
]
\max \mathbb{E}_{\mathbf{z} \sim q}\left[\log p_{\theta}(\boldsymbol{x} | \mathbf{z})\right]
maxEz∼q[logpθ(x∣z)]
第一项优化目标可以理解为约束隐变量𝒛的分布,第二项优化目标理解为提高网络的重建效果。可以看到,经过我们的推导,VAE 模型同样非常地直观好理解。
12.4.2 Reparameterization Trick
现在来考虑上述 VAE 模型在实现时遇到的一个严重的问题。隐变量
z
z
z采样自编码器的输出
q
ϕ
(
z
∣
x
)
q_{\phi}(\mathbf{z} | \boldsymbol{x})
qϕ(z∣x),如图 12.12 左所示。当
q
ϕ
(
z
∣
x
)
q_{\phi}(\mathbf{z} | \boldsymbol{x})
qϕ(z∣x)和𝑝(z)都假设为正态分布时,编码器输出正态分布的均值𝜇和方差𝜎2,解码器的输入采样自𝒩(𝜇, 𝜎2)。由于采样操作的存在,导致梯度传播是不连续的,无法通过梯度下降算法端到端式地训练 VAE 网络。
论文[2]里提出了一种连续可导的解决方案,称为 Reparameterization Trick。它通过 z=
μ
+
σ
⊙
ε
\mu+\sigma \odot \varepsilon
μ+σ⊙ε方式采样隐变量 z,其中
∂
z
∂
μ
\frac{\partial z}{\partial \mu}
∂μ∂z和
∂
z
∂
σ
\frac{\partial z}{\partial \sigma}
∂σ∂z均是连续可导,从而将梯度传播连接起来。如图12.12 右所示,𝜀变量采样自标准正态分布𝒩(0,𝐼),𝜇和𝜎由编码器网络产生,通过z=
μ
+
σ
⊙
ε
\mu+\sigma \odot \varepsilon
μ+σ⊙ε方式即可获得采样后的隐变量,同时保证梯度传播是连续的。
VAE 网络模型如图 12.13 所示,输入𝒙通过编码器网络
q
ϕ
(
z
∣
x
)
q_{\phi}(z | x)
qϕ(z∣x)计算得到隐变量𝒛的均值与方差,通过 Reparameterization Trick 方式采样获得隐变量𝒛,并送入解码器网络,获得分布𝑝𝜃(𝒙|𝒛),并通过式(12-1)计算误差并优化参数。
12.5 VAE 图片生成实战
本节我们基于 VAE 模型实战 Fashion MNIST 图片的重建与生成。如图 12.13 所示,输入为 Fashion MNIST 图片向量,经过 3 个全连接层后得到隐向量𝐳的均值与方差,分别用两个输出节点数为 20 的全连接层表示,FC2 的 20 个输出节点表示 20 个特征分布的均值向量,FC3 的 20 个输出节点表示 20 个特征分布的取log后的方差向量。通过Reparameterization Trick 采样获得长度为 20 的隐向量𝐳,并通过 FC4 和 FC5 重建出样本图片。
VAE 作为生成模型,除了可以重建输入样本,还可以单独使用解码器生成样本。通过从先验分布𝑝(𝐳)中直接采样获得隐向量𝐳,经过解码后可以产生生成的样本。
12.5.1 VAE 模型
我们将 Encoder 和 Decoder 子网络实现在 VAE 大类中,在初始化函数中,分别创建Encoder 和 Decoder 需要的网络层。代码如下:
class VAE(keras.Model):
# 变分自编码器
def __init__(self):
super(VAE, self).__init__()
# Encoder 网络
self.fc1 = layers.Dense(128)
self.fc2 = layers.Dense(z_dim) # 均值输出
self.fc3 = layers.Dense(z_dim) # 方差输出
# Decoder 网络
self.fc4 = layers.Dense(128)
self.fc5 = layers.Dense(784)
Encoder 的输入先通过共享层 FC1,然后分别通过 FC2 与 FC3 网络,获得隐向量分布的均值向量与方差的log向量值。代码如下:
def encoder(self, x):
# 获得编码器的均值和方差
h = tf.nn.relu(self.fc1(x))
# 均值向量
mu = self.fc2(h)
# 方差的 log 向量
log_var = self.fc3(h)
return mu, log_var
Decoder 接受采样后的隐向量𝐳,并解码为图片输出。代码如下:
def decoder(self, z):
# 根据隐藏变量 z 生成图片数据
out = tf.nn.relu(self.fc4(z))
out = self.fc5(out)
# 返回图片数据,784 向量
return out
在 VAE 的前向计算过程中,首先通过编码器获得输入的隐向量𝐳的分布,然后利用Reparameterization Trick 实现的 reparameterize 函数采样获得隐向量𝐳,最后通过解码器即可恢复重建的图片向量。实现如下:
def call(self, inputs, training=None):
# 前向计算
# 编码器[b, 784] => [b, z_dim], [b, z_dim]
mu, log_var = self.encoder(inputs)
# 采样 reparameterization trick
z = self.reparameterize(mu, log_var)
# 通过解码器生成
x_hat = self.decoder(z)
# 返回生成样本,及其均值与方差
return x_hat, mu, log_var
12.5.2 Reparameterization 技巧
Reparameterize 函数接受均值与方差参数,并从正态分布𝒩(0,𝐼)中采样获得𝜀,通过 z = μ + σ ⊙ ε z=\mu+\sigma \odot \varepsilon z=μ+σ⊙ε方式返回采样隐向量。代码如下:
def reparameterize(self, mu, log_var):
# reparameterize 技巧,从正态分布采样 epsion
eps = tf.random.normal(log_var.shape)
# 计算标准差
std = tf.exp(log_var)**0.5
# reparameterize 技巧
z = mu + std * eps
return z
12.5.3 网络训练
网络固定训练 100 个 Epoch,每次从 VAE 模型中前向计算获得重建样本,通过交叉熵损失函数计算重建误差项
E
z
∼
q
[
log
p
θ
(
x
∣
z
)
]
\mathbb{E}_{\mathbf{z} \sim q}\left[\log p_{\theta}(\boldsymbol{x} | \mathbf{z})\right]
Ez∼q[logpθ(x∣z)],根据公式计算
D
K
L
(
q
ϕ
(
z
∣
x
)
∥
p
(
z
)
)
\mathbb{D}_{K L}\left(q_{\phi}(\mathbf{z} | \boldsymbol{x}) \| p(\mathbf{z})\right)
DKL(qϕ(z∣x)∥p(z))误差项,并自动求导和更新整个网络模型。代码如下:
# 创建网络对象
model = VAE()
model.build(input_shape=(4, 784))
# 优化器
optimizer = optimizers.Adam(lr)
for epoch in range(100): # 训练 100 个 Epoch
for step, x in enumerate(train_db): # 遍历训练集
# 打平,[b, 28, 28] => [b, 784]
x = tf.reshape(x, [-1, 784])
# 构建梯度记录器
with tf.GradientTape() as tape:
# 前向计算
x_rec_logits, mu, log_var = model(x)
# 重建损失值计算
rec_loss = tf.nn.sigmoid_cross_entropy_with_logits(labels=x, log
its=x_rec_logits)
rec_loss = tf.reduce_sum(rec_loss) / x.shape[0]
# 计算 KL 散度 N(mu, var) VS N(0, 1)
# 公式参考:https://stats.stackexchange.com/questions/7440/kldivergence-between-two-univariate-gaussians
kl_div = -0.5 * (log_var + 1 - mu**2 - tf.exp(log_var))
kl_div = tf.reduce_sum(kl_div) / x.shape[0]
# 合并误差项
第 12 章 自编码器 16
loss = rec_loss + 1. * kl_div
# 自动求导
grads = tape.gradient(loss, model.trainable_variables)
# 自动更新
optimizer.apply_gradients(zip(grads, model.trainable_variables))
if step % 100 == 0:
# 打印训练误差
print(epoch, step, 'kl div:', float(kl_div), 'rec loss:', float(
rec_loss))
12.5.4 图片生成
图片生成只利用到解码器网络,首先从先验分布𝒩(0,𝐼)中采样获得隐向量,再通过解码器获得图片向量,最后 Reshape 为图片矩阵。例如:
# 测试生成效果,从正态分布随机采样 z
z = tf.random.normal((batchsz, z_dim))
logits = model.decoder(z) # 仅通过解码器生成图片
x_hat = tf.sigmoid(logits) # 转换为像素范围
x_hat = tf.reshape(x_hat, [-1, 28, 28]).numpy() *255.
x_hat = x_hat.astype(np.uint8)
save_images(x_hat, 'vae_images/epoch_%d_sampled.png'%epoch) # 保存生成图片
# 重建图片,从测试集采样图片
x = next(iter(test_db))
logits, _, _ = model(tf.reshape(x, [-1, 784])) # 打平并送入自编码器
x_hat = tf.sigmoid(logits) # 将输出转换为像素值
# 恢复为 28x28,[b, 784] => [b, 28, 28]
x_hat = tf.reshape(x_hat, [-1, 28, 28])
# 输入的前 50 张+重建的前 50 张图片合并,[b, 28, 28] => [2b, 28, 28]
x_concat = tf.concat([x[:50], x_hat[:50]], axis=0)
x_concat = x_concat.numpy() * 255. # 恢复为 0~255 范围
x_concat = x_concat.astype(np.uint8)
save_images(x_concat, 'vae_images/epoch_%d_rec.png'%epoch) # 保存重建图片
图片重建的效果如图 12.15、图 12.16、图 12.17 所示,分别显示了在第 1、10、100个 Epoch 时,输入测试集的图片,获得的重建效果,每张图片的左 5 列为真实图片,右 5列为对应的重建效果。图片的生成效果图 12.18、图 12.19、图 12.20 所示,分别显示了在第 1、10、100 个 Epoch 时,图片的生成效果。
可以看到,图片重建的效果是要略好于图片生成的,这也说明了图片生成是更为复杂的任务,VAE 模型虽然具有图片生成的能力,但是生成的效果仍然不够优秀,人眼还是能够较轻松地分辨出机器生成的和真实的图片样本。下一章将要介绍的生成对抗网络在图片生成方面表现更为优秀。