图像生成系列(二)——Variational Autoencoder

1,787 阅读10分钟

知识准备

Variational Autoencoder(VAE)是一种生成模型,用于学习输入数据的潜在表示并生成与原始数据相似的新样本。它结合了自动编码器和概率推断的概念。VAE 的结构包括两个主要部分:编码器(Encoder)和解码器(Decoder)。Encoder 将输入数据映射到潜在低维空间,并输出潜在变量的均值和方差。这些统计量用于定义潜在变量的概率分布。Decoder 接收从潜在低维空间采样的潜在变量,并将其映射回原始数据空间,生成与输入数据相似的重构数据。

VAE 的训练过程通过最大化观测数据的边际似然来实现。这个过程同时优化了两个损失函数:重构损失和正则化损失。重构损失衡量了解码器重建数据与原始数据之间的差异,促使模型学习生成接近输入数据的重建样本。正则化损失通过最小化潜在变量的分布与先验分布之间的差异来促使 Encoder 学习将输入数据编码为合理的潜在表示。常用的先验分布是高斯分布。

VAE 常见具有以下功能:

  1. 生成样本:VAE 可以从潜在空间中采样,并将采样的潜在变量解码为新的样本。这使得模型能够生成与训练数据相似的新样本。
  2. 数据压缩:VAE 可以将输入数据压缩为潜在变量的表示,这个表示通常是低维的。这有助于减少数据的维度并提取关键特征。这类似于 AE 模型。
  3. 插值和描绘:在潜在空间中,通过对两个不同潜在变量之间的线性插值进行解码,可以生成介于两个样本之间的新样本。这提供了一种可视化模型对样本特征的理解方式。

VAE 是一种强大的生成模型,广泛应用于图像生成、特征学习、数据压缩等任务。它通过结合自动编码器和概率推断的思想,实现了从输入数据到潜在空间的编码和从潜在空间到输出数据的解码,为生成模型提供了一种有效的建模方法。

v2-bb5516b570a385276748531f069febd2_r.png

数据准备

这里的代码主要是对数据的处理,主要如下:

  1. 定义了一些参数,包括训练集的样本数量、批次大小和测试集的样本数量等。
  2. 定义了一个函数 preprocess_images,用于对图像数据进行预处理。函数将图像数据的形状调整为 (B, 28, 28, 1),并将像素值归一化到 0~1 的范围内。
  3. 使用 tf.keras.datasets.mnist.load_data() 加载 MNIST 手写数字数据集,将训练集和测试集分别赋值给 train_imagestest_images
  4. 对训练集和测试集的图像数据进行预处理,调用 preprocess_images 函数对图像数据进行处理,并将处理后的数据保存到 train_imagestest_images 中。
  5. 使用 tf.data.Dataset.from_tensor_slices() 创建训练集和测试集的数据集对象,将处理后的图像数据传入,并设置数据集的 shuffle 和 batch 大小。
import tensorflow as tf
import numpy as np
from tensorflow.keras import Model
import matplotlib.pyplot as plt
import time
from IPython import display
import os
import imageio
import glob
train_size = 60000
batch_size = 32
test_size = 10000
def preprocess_images(images):
    images = images.reshape((images.shape[0], 28, 28, 1)) / 255.0
    return np.where(images > 0.5, 1.0, 0.0).astype('float32')
(train_images, _), (test_images, _) = tf.keras.datasets.mnist.load_data()
train_images = preprocess_images(train_images)
test_images = preprocess_images(test_images)
train_dataset = (tf.data.Dataset.from_tensor_slices(train_images).shuffle(train_size).batch(batch_size))
test_dataset = (tf.data.Dataset.from_tensor_slices(test_images).shuffle(test_size).batch(batch_size))

这是一个工具类,主要是对图片的展示。

def show(img):
    plt.figure()
    plt.imshow(img)
    plt.show()

模型结构

这里定义了一个变分自编码器(Variational Autoencoder,VAE)的类 VAE,继承自 tf.keras.Model。在 VAE 类的构造函数中,进行了如下操作:

  1. 定义了变分自编码器的潜在空间维度为 2 。
  2. 创建了编码器(encoder)模型,使用了一系列的卷积层和全连接层来对输入图像进行编码。编码器的最后一层输出包括均值(mean)和方差(logvar)两部分。
  3. 创建了解码器(decoder)模型,使用了一系列的全连接层和反卷积层来对潜在变量进行解码,最终重构出与原始图像相似的图像。
  4. 实现了 sample 方法,用于从潜在空间中生成图像样本。可以传入一个随机噪声 eps,若未指定则使用随机正态分布生成。
  5. 实现了 encode 方法,用于对输入图像进行编码,返回均值和方差。
  6. 实现了 reparameterize 方法,用于对均值和方差进行重新参数化,生成潜在变量。
  7. 实现了 decode 方法,用于将潜在变量解码成图像。

该类的主要功能是通过编码器将输入图像映射到潜在空间,再通过解码器将潜在变量映射回图像空间,从而实现图像的重构和生成。同时,通过引入潜在变量的均值和方差,并通过重新参数化技巧进行采样,使得模型能够生成具有多样性的图像样本。

class VAE(Model):
    def __init__(self):
        super(VAE, self).__init__()
        self.latent_dim = 2
        # encoder
        self.encoder = tf.keras.Sequential([
            tf.keras.layers.InputLayer(input_shape=(28, 28, 1)),
            tf.keras.layers.Conv2D(filters=32, kernel_size=3, strides=(2, 2), activation='relu'),
            tf.keras.layers.Conv2D(filters=64, kernel_size=3, strides=(2, 2), activation='relu'),
            tf.keras.layers.Flatten(),
            tf.keras.layers.Dense(self.latent_dim + self.latent_dim)
        ])
        # decoder
        self.decoder = tf.keras.Sequential([
            tf.keras.layers.InputLayer(input_shape=(self.latent_dim,)),
            tf.keras.layers.Dense(units=7*7*32, activation=tf.nn.relu),
            tf.keras.layers.Reshape(target_shape=(7, 7, 32)),
            tf.keras.layers.Conv2DTranspose(filters=64, kernel_size=3, strides=2, padding='same', activation='relu'),
            tf.keras.layers.Conv2DTranspose(filters=32, kernel_size=3, strides=2, padding='same', activation='relu'),
            tf.keras.layers.Conv2DTranspose(filters=1, kernel_size=3, strides=1, padding='same')
        ])
    @tf.function
    def sample(self, eps=None):
        if eps is None:
            eps = tf.random.normal(shape=(100, self.latent_dim))
        return self.decode(eps, apply_sigmoid=True)
    def encode(self, x):
        mean, logvar = tf.split(self.encoder(x), num_or_size_splits=2, axis=1)
        return mean, logvar
    def reparameterize(self, mean, logvar):
        eps = tf.random.normal(shape=mean.shape)
        return eps * tf.exp(logvar * .5) + mean
    def decode(self, z, apply_sigmoid=False):
        logits = self.decoder(z)
        if apply_sigmoid:
            probs = tf.sigmoid(logits)
            return probs
        return logits

模型训练

这段代码定义了一个 Adam 优化器,并定义了一个函数 log_normal_pdf

  1. optimizer = tf.keras.optimizers.Adam(1e-4) 创建了一个 Adam 优化器对象,并设置学习率为 0.0001 。

  2. log_normal_pdf(sample, mean, logvar, raxis=1) 是一个函数,用于计算潜在变量采样值在给定均值和方差下的对数正态分布的概率密度函数(probability density function,PDF)。在函数内部,首先计算 log2pi ,然后根据对数正态分布的公式,计算采样值的对数概率密度。最后在指定维度上求和并返回结果。

  3. compute_loss 用于计算变分自编码器(VAE)模型的损失函数。首先,通过调用模型的 encode 方法,计算输入数据 x 的均值(mean)和方差的对数(logvar)。接下来,通过调用模型的 reparameterize 方法,使用均值和方差的对数,生成潜在变量 z。然后使用模型的 decode 方法,将潜在变量 z 解码为重构的输出 x_logit。使用 tf.nn.sigmoid_cross_entropy_with_logits 函数,计算重构输出 x_logit 与输入数据 x 之间的交叉熵损失。将交叉熵损失在各个维度上求和,得到 logpx_z。调用之前定义的 log_normal_pdf 函数,计算潜在变量 z 在标准正态分布下的对数概率密度函数值,得到 logpz。调用 log_normal_pdf 函数,计算潜在变量 z 在给定均值和方差的对数下的对数概率密度函数值,得到 logqz_x。计算最终的损失函数,即 logpx_z + logpz - logqz_x 的负数平均值。

  4. train_step 用于执行单个训练步骤。在函数中,使用 @tf.function 装饰器将函数转换为 TensorFlow 的图执行模式,以提高训练的效率。函数中主要是先计算损失,然后为可训练的权重计算梯度,并使用反向传播进行权重的更新。

optimizer = tf.keras.optimizers.Adam(1e-4)
def log_normal_pdf(sample, mean, logvar, raxis=1):
    log2pi = tf.math.log(2.0 * np.pi)
    return tf.reduce_sum(-.5 * ((sample - mean) ** 2. * tf.exp(-logvar) + logvar + log2pi), axis=raxis)

def compute_loss(model, x):
    mean, logvar = model.encode(x)
    z = model.reparameterize(mean, logvar)
    x_logit = model.decode(z)
    cross_ent = tf.nn.sigmoid_cross_entropy_with_logits(logits=x_logit, labels=x)
    logpx_z = -tf.reduce_sum(cross_ent, axis=[1, 2, 3])
    logpz = log_normal_pdf(z, 0.0, 0.0)
    logqz_x = log_normal_pdf(z, mean, logvar)
    return -tf.reduce_mean(logpx_z + logpz - logqz_x)

@tf.function
def train_step(model, x, optimizer):
    with tf.GradientTape() as tape:
        loss = compute_loss(model, x)
    gradients = tape.gradient(loss, model.trainable_variables)
    optimizer.apply_gradients(zip(gradients, model.trainable_variables))
  1. generate_and_save_images 函数是个工具函数,主要是将模型生成的图像进行一个 16 格的展示,在每次训练完一次 epoch ,就会将事先准备好的 16 张图片传入模型中,让 VAE 模型生成原始图片,并且保存下来。
  2. 不断进行模型的训练,不断进行模型的效果展示。
epochs = 10
num_sample = 16 # 要小于 batch_size
model = VAE()
def generate_and_save_images(model, epoch, test_sample):
    mean, logvar = model.encode(test_sample)
    z = model.reparameterize(mean, logvar)
    predictions = model.sample(z)
    fig = plt.figure(figsize=(4,4))
    for i in range(predictions.shape[0]):
        plt.subplot(4,4,i+1)
        plt.imshow(predictions[i, :, :, 0])
        plt.axis('off')
    plt.savefig('image_at_epoch_{:04d}.png'.format(epoch))
    plt.show()
    
for test_batch in test_dataset.take(1):
    test_sample = test_batch[0:num_sample, :, :, :]
    
generate_and_save_images(model, 0, test_sample)
for epoch in range(1, epochs + 1 ):
    start_time = time.time()
    for train_x in train_dataset:
        train_step(model, train_x, optimizer)
    end_time = time.time()
    loss = tf.keras.metrics.Mean()
    for test_x in test_dataset:
        loss(compute_loss(model, test_x))
    elbo = -loss.result()
    display.clear_output(wait=False)
    print('Epoch: {}, Test set ELBO: {}, time elapse for current epoch: {}' .format(epoch, elbo, end_time - start_time))
    generate_and_save_images(model, epoch, test_sample)

结果打印:

Epoch: 10, Test set ELBO: -155.91403198242188, time elapse for current epoch: 4.749928712844849

image.png

我将所有 epoch 产生的图像制作成了一张 gif ,大家可以看出整个训练过程的效果实在逐渐变强的。

vae_result.gif

VAE 的优缺点

优点:

  1. 生成高质量样本:VAE 能够生成与训练数据相似的高质量样本,可以用于图像生成等任务。
  2. 潜在空间连续性:VAE 的潜在空间具有连续性,可以通过在潜在空间中进行插值操作生成具有平滑变化的样本。
  3. 数据压缩和特征学习:VAE 可以将输入数据压缩为潜在变量的表示,从中学习到数据的关键特征。

缺点:

  1. 潜在空间不一定具有可解释性:尽管潜在空间是连续的,但其中的特定维度可能没有直接的语义解释,使得对潜在空间的理解和解释变得困难。
  2. 模糊的重建图像:由于使用了概率性的重建过程,VAE 生成的重建图像可能比输入图像更模糊,且无法保证完美的重建。
  3. 训练复杂度高:VAE 模型的训练相对复杂,需要同时优化重构损失和正则化损失,并且需要仔细选择合适的超参数。

VAE 与 AE 进行比较

我们上文已经介绍了 AE ,它和 VAE 很像,VAE 和 AE 是两种常见的无监督学习模型,用于数据压缩、特征学习和生成模型等任务。下面比较相似之处和不同之处。

相似之处:

  1. 基本结构:VAE 和 AE 都由 Encoder 和 Decoder 组成。Encoder 将输入数据映射到潜在空间中的编码表示,Decoder 则将潜在空间中的编码恢复为重构的输出数据。
  2. 无监督学习:VAE 和 AE 都是无监督学习模型,不需要标注的训练数据,只使用输入数据本身进行训练。

不同之处:

  1. 潜在空间的表示:AE 的潜在空间是确定性的,即编码后的表示是确定的固定向量。而 VAE 的潜在空间是概率性的,即编码后的表示是均值和方差的分布,使得潜在空间具有连续性和采样性质。
  2. 模型训练目标:AE 通过最小化重构误差来学习数据的低维表示,即尽量恢复输入数据本身。而 VAE 不仅要最小化重构误差,还要最大化潜在空间的先验分布与编码后的分布之间的相似性,通过最小化重构损失和正则化损失来训练模型。
  3. 生成能力:由于 VAE 的潜在空间具有连续性和采样性质,可以从潜在空间中采样生成新的样本,具有一定的创造性和多样性。而 AE 没有显式的生成过程,只能通过编码和解码的过程来重构输入数据。

感谢

tensorflow.google.cn/tutorials/g…