1、变分自编码器
基本的自编码器本质上是学习输入𝒙和隐藏变量𝒛之间映射关系,它是一个判别模型 (Discriminative model),并不是生成模型(Generative model)。那么能不能将自编码器调整为 生成模型,方便地生成样本呢?
给定隐藏变量的分布P(𝒛),如果可以学习到条件概率分布P(𝒙|𝒛),则通过对联合概率 分布P(𝒙, 𝒛) = P(𝒙|𝒛)P(𝒛)进行采样,生成不同的样本。变分自编码器(Variational AutoEncoders,简称 VAE)就可以实现此目的,如图 所示。如果从神经网络的角度来理解 的话,VAE 和前面的自编码器一样,非常直观好理解;但是 VAE 的理论推导稍复杂,接下来我们先从神经网络的角度去阐述 VAE。
从神经网络的角度来看,VAE 相对于自编码器模型,同样具有编码器和解码器两个子网络。解码器接受输入𝒙,输出为隐变量𝒛;解码器负责将隐变量𝒛解码为重建的𝒙 。不同的 是,VAE 模型对隐变量𝒛的分布有显式地约束,希望隐变量𝒛符合预设的先验分布P(𝒛)。因 此,在损失函数的设计上,除了原有的重建误差项外,还添加了隐变量𝒛分布的约束项。
2、Reparameterization Trick
现在来考虑上述 VAE 模型在实现时遇到的一个严重的问题。隐变量采样自编码器的 输出$q_\phi(z|x)$,如图左所示。当$q_\phi(z|x)$和𝑝( z)都假设为正态分布时,编码器输出正态分布的均值𝜇和方差𝜎 2,解码器的输入采样自𝒩(𝜇, 𝜎 2 )。由于采样操作的存在,导致梯 度传播是不连续的,无法通过梯度下降算法端到端式地训练 VAE 网络。
论文:Auto-Encoding Variational Bayes里提出了一种连续可导的解决方案,称为 Reparameterization Trick。它它通过 = 𝜇 + 𝜎 ⊙ 𝜀方式采样隐变量 z,其中$\frac{\partial z}{\partial \mu } $和$\frac{\partial z}{\partial \sigma } $均是连续可导,从而将梯度传播连接起来。如图 右所示,𝜀变量采样自标准正态分布𝒩(0,𝐼),𝜇和𝜎由编码器网络产生,通过 = 𝜇 + 𝜎 ⊙ 𝜀方式即可获得采样后的隐变量,同时保证梯度传播是连续的。
VAE 网络模型如下图所示,输入𝒙通过编码器网络$q_\phi(z|x)$计算得到隐变量𝒛的均值与方差,通过 Reparameterization Trick 方式采样获得隐变量𝒛,并送入解码器网络,获得 分布$p_\theta (x|z)$,并通过式$\iota (\theta ,\phi )=-D_{KL}(q_\phi (z|x)||p(z))+E_{z\sim q}[\log_{}{p_\theta (x|z)} ]$计算误差并优化参数。
3、VAE图片生成实战
我们基于 VAE 模型实战 Fashion MNIST 图片的重建与生成。如图所示,==输 入为 Fashion MNIST 图片向量,经过 3 个全连接层后得到隐向量𝐳的均值与方差,分别用两 个输出节点数为 20 的全连接层表示,FC2 的 20 个输出节点表示 20 个特征分布的均值向量 ,FC3 的 20 个输出节点表示 20 个特征分布的取log后的方差向量。通过 Reparameterization Trick 采样获得长度为 20 的隐向量𝐳,并通过 FC4 和 FC5 重建出样本图片。==
VAE 作为生成模型,除了可以重建输入样本,还可以单独使用解码器生成样本。通过 从先验分布𝑝(𝐳)中直接采样获得隐向量𝐳,经过解码后可以产生生成的样本。
3.1 VAE模型
我们将 Encoder 和 Decoder 子网络实现在 VAE 大类中,在初始化函数中,分别创建 Encoder 和 Decoder 需要的网络层。
Encoder 的输入先通过共享层 FC1,然后分别通过 FC2 与 FC3 网络,获得隐向量分布 的均值向量与方差的log向量值。
Decoder 接受采样后的隐向量z,并解码为图片输出。
在 VAE 的前向计算过程中,首先通过编码器获得输入的隐向量𝐳的分布,然后利用 Reparameterization Trick 实现的 reparameterize 函数采样获得隐向量𝐳,最后通过解码器即可 恢复重建的图片向量。
#VAE模型
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接收采样后的隐向量z,并解码为图片输出
def decoder(self,z):
#根据隐藏变量z生成图片数据
out=tf.nn.relu(self.fc4(z))
out=self.fc5(out)
#返回图片数据,784向量
return out
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
3.2 Reparameterization技巧
Reparameterize 函数接受均值与方差参数,并从正态分布𝒩(0,𝐼)中采样获得𝜀,通过 = 𝜇 + 𝜎 ⊙ 𝜀方式返回采样隐向量。代码如下:
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
3.3 网络训练
网络固定训练 100 个 Epoch,每次从 VAE 模型中前向计算获得重建样本,通过交叉熵损失函数计算重建误差项$E_{z\sim q}[\log_{}{p_\theta (x|z)} ]$,根据公式$D_{KL}(q_\phi (z|x)||p(z))=-log\delta _1+0.5\delta _1^2+0.5\mu _1^2-0.5$计算$D_{KL}(q_\phi (z|x)||p(z))$误差项,并自动求导和更新整个网络模型。
#网络训练
#创建网络对象
model=VAE()
model.build(input_shape=(4,784))
#优化器
optimizer=tf.keras.optimizers.Adam(lr)
for epoch in range(100):
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,logits=x_rec_logits)
rec_loss=tf.reduce_sum(rec_loss)/x.shape[0]
#计算KL散度N(mu,var) VS N(0,1)
kl_div=-0.5*(log_var+1-mu**2-tf.exp(log_var))
kl_div=tf.reduce_sum(kl_div)/x.shape[0]
#合并误差项
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))
3.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/sampled_epoch%d.png'%epoch)
#重建图片
x = next(iter(test_db))
x = tf.reshape(x, [-1, 784])
x_hat_logits, _, _ = model(x)
x_hat = tf.sigmoid(x_hat_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/rec_epoch%d.png'%epoch)
图片重建的效果如图所示,分别显示了在第 1、10、100 个 Epoch 时,输入测试集的图片,获得的重建效果,每张图片的左 5 列为真实图片,右 5 列为对应的重建效果。图片重建的效果如图所示,分别显示了在第 1、10、100 个 Epoch 时,输入测试集的图片,获得的重建效果,每张图片的左 5 列为真实图片,右 5 列为对应的重建效果。
可以看到,图片重建的效果是要略好于图片生成的,这也说明了图片生成是更为复杂 的任务,VAE 模型虽然具有图片生成的能力,但是生成的效果仍然不够优秀,人眼还是能够较轻松地分辨出机器生成的和真实的图片样本。
4、完整代码
import os
import tensorflow as tf
import numpy as np
from tensorflow import keras
from tensorflow.keras import Sequential, layers
from PIL import Image
from matplotlib import pyplot as plt
import tensorflow.keras as keras
tf.random.set_seed(22)
np.random.seed(22)
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
assert tf.__version__.startswith('2.')
def save_images(imgs, name):
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)
h_dim = 20
batchsz = 512
lr = 1e-3
(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.
# we do not need label
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)
print(x_train.shape, y_train.shape)
print(x_test.shape, y_test.shape)
z_dim = 10
#VAE模型
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接收采样后的隐向量z,并解码为图片输出
def decoder(self,z):
#根据隐藏变量z生成图片数据
out=tf.nn.relu(self.fc4(z))
out=self.fc5(out)
#返回图片数据,784向量
return out
#Reparameterize
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
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
#网络训练
#创建网络对象
model=VAE()
model.build(input_shape=(4,784))
#优化器
optimizer=tf.keras.optimizers.Adam(lr)
for epoch in range(100):
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,logits=x_rec_logits)
rec_loss=tf.reduce_sum(rec_loss)/x.shape[0]
#计算KL散度N(mu,var) VS N(0,1)
kl_div=-0.5*(log_var+1-mu**2-tf.exp(log_var))
kl_div=tf.reduce_sum(kl_div)/x.shape[0]
#合并误差项
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))
# evaluation
#生成图片
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/sampled_epoch%d.png'%epoch)
#重建图片
x = next(iter(test_db))
x = tf.reshape(x, [-1, 784])
x_hat_logits, _, _ = model(x)
x_hat = tf.sigmoid(x_hat_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/rec_epoch%d.png'%epoch)
生成和重建的图片如下:
参考:《TensorFlow深度学习——深入理解人工智能算法设计》