《GANs实战》学习笔记(二)第二章 自编码器生成模型入门

GAN(生成对抗网络)出版了一本实战书,了解下?

第二章 自编码器生成模型入门

1、生成模型简介-潜在空间-Latent space

如果有一定的深度学习基础,肯定会对“深度学习如何额获取图像中的原始像素并将其转化为类别预测”这种操作不陌生。例如,可以取包含图像像素的3个矩阵(每个颜色通道各一个)在一个转换系统中传递,最后得到一个数字。如果想反过来做,该怎么办?

生成模型:从要生成内容的描述指令(prescription)开始,最后在转换系统的另一端得到图像。

更正式的表述:取一个特定的描述指令(z)——简单的假设它是介于0和9之间的数字——并尝试得到一个生成的样本(x^*{})。理想情况下,x^*{}应该和另一个真实的样本x看起来一样真实。

描述指令z潜在空间(latent space)中的某个激励,我们不会总是得到相同的输出x^*{}。这个潜在空间是一个习得的表征——希望它按人类思考方式对人们有意义(“解离”)。

不同的模型将学习相同的数据的不同潜在表征

以上内容引自中文书,真费劲,咱们看英语原文吧。

prescription:处方,药方;惯例;指示。前缀pre-表示时间上的“先”。医生先(pre-)写(scrib-)好处方,病人随后按医生指示拿药。

英文原文:We start with a prescription of what we want to produce and get the image at the other end of the transformations. 我们要在转换系统(生成模型)的另一端获得我们想要生成的图像,必须要先有一个“药方,指引”。

A bit more formally, we take a certain prescription (z)—for this simple case, let’s say it is a number between 0 and 9—and try to arrive at a generated sample (x*). Ideally, this x* would look as realistic as another real sample, x. The prescription, z, lives in a latent space and serves as an inspiration so that we do not always get the same output, x*. This latent space is a learned representation—hopefully meaningful to people in ways we think of it (“disentangled”). Different models will learn a different latent representation of the same data.

第1章中的随机噪声向量统称被称为来自潜在空间的样本。 潜在空间是数据点的一种更简单的隐式表示,本书中用z来表示,意味着更低的维度

a vector or array of 100 numbers rather than the 768 that is the dimensionality of the samples we will use.

2、自编码器如何用于高级场景

自编码器由两部分神经网络组成:编码器解码器

举例:压缩。

我们每天都在压缩数据(信息),这样就不会花很长时间去解释已知的概念。自编码器做的就是这件事。

但是不同场景的自编码器不同:讨论我们的工作,我们向祖父母解释的东西不必向我们的同事解释,比如机器学习模型是什么。讨论我们家的猫,我们向同事解释的东西,不必再向我们的父母解释。

不同情景的不同对象的潜在空间不同,因此“自编码器”也不同,但是针对不同场景我们会选择不同的简单表示方式。这个简单表示方式组成的空间,或者说语境,就是潜在的,隐藏的Latent space。潜台词所在的语境。

我们可以将某些重复出现的概念简化为已达成共识抽象概念 。这个简化转化模型就是编码压缩信息传输模式。自编码器可以系统地自动发现这些高效信息模式,对其进行定义,并将它们用作快捷方式来提高信息吞吐量。最终效果:我们只需要传输z即可——这个z是低维的,节省了带宽。

跟同事讨论工作,直接说参数就可以,不需要解释参数背后的意思,跟父母说,就必须讲解所有名词背后的意思和原理。

跟父母讨论家里猫的状况,直接说打开窗子,跟同事说,就必须解释为什么要打开窗子。

潜在空间:是数据的隐式表示。自编码器不是再未压缩的版本中表达单词或图像(例如机器学习工程师,或图像的JPEG编解码器),而是根据对数据的理解来对其进行压缩和聚类特征提取

就像是互联网黑话:赋能、链路、内卷等。跟明白人说,明白就是明白,跟不明白人说,不明白就需要解释。编码器和解码器对应的上,语言语种相通。

3、什么是GAN的自编码器

自编码器与GAN的一个关键区分点:我们用一个损失函数对整个自编码器网络进行端到端的训练,而GAN的生成器和鉴别器分别有损失函数

4、自编码器的构成

自编码器步骤:

(1)编码器网络:取一个表示x(比如一张图像),然后用学过的编码器(通常是一个单层或者多层的神经网络)将维数从y减小到z。

(2)潜在空间(z):在训练时,试图建立具有一定意义的潜在空间。潜在空间通常是有较小维度的表示,起着中间步骤的作用。在这种数据的表示中,自编码器试图“组织其思想”。

(3)解码器网络:用解码器将数原始对象重建到原始的维度,这通常由一个神经网络完成。它是编码器的镜像,对应着从z到 x^*{}的步骤。我们可以应用解码的逆过程,从潜在空间的256个像素长的向量中得到784个像素长的重构向量(28×28大小的图像)。

自编码器训练过程:

(1)将图像x通过自编码器输入。

(2)得到x^*{},即重建的图像。

(3)评估重建损失,即x和x^*{}之间的差异:

  • 使用图像x和x^*{}的像素之间的距离(如MAE)完成;

  • 给一个现实的目标函数(||x-x^*{}||),以通过梯度下降的形式进行优化。

我们的任务就是通过梯度下降来更新编码器和解码器的参数,使损失函数最小化。(原文这句翻译让人。。,我重新翻译。)

5、自编码器的应用

(1)压缩、降维。低维表示可以提高许多任务的性能,例如分类等。

(2)信息检索(information retrieval)从降维中获益更多。异常检测(anomaly-detection)

(3)黑白图去噪或彩色化

(4)作为GANs网络结构的一部分,来稳定训练。

(5)训练自编码器不需要带标签的数据

(6)生成新图像。

6、无监督学习

无监督学习:是一种从数据本身学习而不需要关于这些数据含义的附加标签的机器学习。

对于许多压缩类型的任务:带标记的数据就是要训练的数据本身。这类数据唯一的标签就是样本本身或者数据集中的任何其他样本。也称为自监督

6.1 吐故纳新

自编码器由两个神经网络组成:编码器和解码器。

本书案例中,编码器和解码器都有激活函数,且只为每个函数使用一个中间层。这意味着每个网络有两个权重矩阵:

  • 编码器:
    • 从输入到中间层
    • 从中间层到潜在空间
  • 解码器:
    • 从潜在空间到另一个中间层
    • 从中间层到输出。

6.2 VAE变分自编码器

Variational auto-encoder

7、代码即生命

原书代码

请下载原代码,要比咱们书上注释的全面。安装jupyter,在jupyter中打开源码中的ipynb文件。

示例目的:基于潜在空间生成手写数字。

思路:通过生成器generator或者解码器decoder用predict()方法通过潜在空间的一个向量来生成一个新的手写数字。

(1)导包,import所有依赖项。

from __future__ import print_function

import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import norm
import tensorflow as tf

from keras.layers import Input, Dense, Lambda, Reshape
from keras.models import Model
from keras import backend as K
from keras import metrics
from keras.datasets import mnist

(2)设置全局变量和超参数

# defining the key parameters
batch_size = 100
original_dim = 784
latent_dim = 2
intermediate_dim = 256
epochs = 50
epsilon_std = 1.0

(3)Keras函数式API

Keras有两种构建model的方式,一种是Sequential贯序式,一种是函数式。Sequential并不适用于分支结构,本例中对样本的均值和方差两个特征加入到潜在空间 z(学习的是均值和方差),存在分支复用,所以用函数式。

函数式API

前面的层作为参数传递给后面的层。为什么可以如此?

层的实例是可调用的,它以张量为参数,并且返回一个张量。

函数式案例如下:(与本书案例代码无关,只为阐述复习Kears内容,为防止误操作,截图处理。) 

所以说每一层本质就是张量,输入为张量,输出也是张量,所以利用函数式API,可以轻易的重用训练好的模型:可以将模型看做是一个层,每一层其实就是一个张量,然后通过传递一个张量来调用它。

注意在调用模型时,你不仅重用了模型的结构,还重用了它的权重。 

(4)定义采样辅助函数

从构建的高斯分布中才用得到Latent vector。

从潜在空间 z 采样后将信息传递给解码器,即将z_mean和z_log_var正态分布的定义特征——均值方差传递给解码器。解码器从Latent Vector中生成数据。

def sampling(args: tuple):
    # we grab the variables from the tuple
    z_mean, z_log_var = args
    epsilon = K.random_normal(shape=(K.shape(z_mean)[0], latent_dim), mean=0.,
                              stddev=epsilon_std)
    return z_mean + K.exp(z_log_var / 2) * epsilon

关于Keras中K.random_normal的应用。 生成正态分布的随机数构成的张量。

 

 (5)定义编码器encoder

# input to our encoder
x = Input(shape=(original_dim,), name="input")
# intermediate layer
h = Dense(intermediate_dim, activation='relu', name="encoding")(x)
# defining the mean of the latent space
z_mean = Dense(latent_dim, name="mean")(h)
# defining the log variance of the latent space
z_log_var = Dense(latent_dim, name="log-variance")(h)
# note that "output_shape" isn't necessary with the TensorFlow backend
z = Lambda(sampling, output_shape=(latent_dim,))([z_mean, z_log_var])
# defining the encoder as a keras model
encoder = Model(x, [z_mean, z_log_var, z], name="encoder")
# print out summary of what we just did
encoder.summary()

本着每一步必须弄明白的较真:

x是输入层Input来构造。h是中间隐藏层,此时类似h(x)效果,输出为h张量,但是h之后,分支了,为何分支,因为有两个地方都调用了h。h这个张量被两个地方调用,所以原来一条直线的拓扑变成了分支结构。

接着有一个Lambda层Layer层,也就是z,它又把分支合并了。

来进一步看一下Lambda层。从导入包知道Lambda属于Layer。

from keras.layers import Input, Dense, Lambda, Reshape

它的作用:Keras官网关于Lambda

 

看一下:模型各层的参数状况。

514同理:是因为(256+1)× 2 = 514。同样带有偏置b。无论mean均值分支层还是log-variance对数方差分支层,都带有偏置。

神经元数是从输入的32×32=784,经过一个隐藏层变成256。隐藏层要被利用两次,分支分别进mean层和log-variance层,神经元数分别变成2。也就是说变成了2组包含了只有latent_dim=2个神经元的分支层。最后分支层到达Lambda层,2个2维的分支层变成只有一个2元层。

神经元数从784变成256,然后从256变成两组分别2个神经元,最后两组分别的2个神经元,变成一组2个神经元。

画个拓扑图看一下:

# visualize model layout with pydot_ng
from keras.utils import plot_model
import os

os.environ["PATH"] += os.pathsep + 'C:/Program Files/Graphviz/bin/'
plot_model(encoder, to_file='./model2.png', show_shapes=True)

 

 至此,编码器只有两个神经元。(关于均值和对数方差的潜在空间)潜在空间层上的这两个神经元将作为解码器的开始。

(6)编写解码器

# Input to the decoder
input_decoder = Input(shape=(latent_dim,), name="decoder_input")
# taking the latent space to intermediate dimension
decoder_h = Dense(intermediate_dim, activation='relu', name="decoder_h")(input_decoder)
# getting the mean from the original dimension
x_decoded = Dense(original_dim, activation='sigmoid', name="flat_decoded")(decoder_h)
# defining the decoder as a keras model
decoder = Model(input_decoder, x_decoded, name="decoder")
decoder.summary()

编码器最终是两个神经元,解码器网络的起始位置就是这两个神经元。 

(7)组合模型

把编码器和解码器组合成为一个VAE模型。看不懂,不明白,decoder(encoder(x)[2])直接干蒙圈了。

因为编码器和解码器都有一层是两个神经元,也就是说编码器的第四层和解码器的第一层都是2个神经元,这个是重复的,在此处合而为一。

Recall一下,在编码器中Model的output为一个列表:[z_mean, z_log_var, z]。

encoder = Model(x, [z_mean, z_log_var, z], name="encoder")

 

encoder(x)代表将x作为输入Input,输入到编码器定义的Model中,输出为一个长度为3的列表,列表的第三个元素也就是编号[2],代表的是z。二维的z,我在回顾一下编码器中z的定义:z是编码器对输入构建高斯分布之后采样得到的Latent vector

请牢记:Keras函数式API,每一层Layer都是一个张量。只要输入输出满足,就可以连接。每一层作为一个参数直接输入到下一层。

# grab the output. Recall, that we need to grab the 3rd element our sampling z
output_combined = decoder(encoder(x)[2])
# link the input and the overall output
vae = Model(x, output_combined)
# print out what the overall model looks like
vae.summary()

通过Keras.Model构造了一个编码器和解码器组合起来的整个神经网络。 

(8)定义损失函数

def vae_loss(x: tf.Tensor, x_decoded_mean: tf.Tensor, z_log_var=z_log_var, z_mean=z_mean, original_dim=original_dim):
    xent_loss = original_dim * metrics.binary_crossentropy(x, x_decoded_mean)
    kl_loss = - 0.5 * K.sum(1 + z_log_var - K.square(z_mean) - K.exp(z_log_var), axis=-1)
    vae_loss = K.mean(xent_loss + kl_loss)
    return vae_loss

 二元交叉熵KL散度加在一起组成整体损失。

KL散度衡量两个分布之间距离的差异度量。二元交叉熵是二分类常见的损失函数之一。

 

损失函数两部分组成: 自编码器本身自带的generation_loss生成loss、和VAE的Latent_loss。

生成损失:MSE(均方误差):差值,平方,均值。

KL散度:数据分布度量。

(9)编译模型 

vae.compile(optimizer='rmsprop', loss=vae_loss)

优化器一般选择Adam、SGD、RMSprop这3种方法,很少尝试其他的方法。

梯度更新方式。 

(10)拆分训练集、测试集

拆分训练集、测试集和输入归一化。

(x_train, y_train), (x_test, y_test) = mnist.load_data()

x_train = x_train.astype('float32') / 255.
x_test = x_test.astype('float32') / 255.
x_train = x_train.reshape((len(x_train), np.prod(x_train.shape[1:])))
x_test = x_test.reshape((len(x_test), np.prod(x_test.shape[1:])))

 在jupyter中查看这个输入的shape:拿x_test为例。

from keras.datasets import mnist

(x_train, y_train), (x_test, y_test) = mnist.load_data()
x_test.shape

 输出为:(10000, 28, 28)

10000个样本,每个样本维度为28乘以28的图像。np.prod将后面两位维度相乘flatten。为784。

(11)训练模型

vae.fit(x_train, x_train, shuffle=True, epochs=epochs, batch_size=batch_size)

训练好之后两个子模型也是训练好的。即encoder和decoder都是可以直接拿来用的。 

(12)潜在空间可视化

# display a 2D plot of the digit classes in the latent space
x_test_encoded = encoder.predict(x_test, batch_size=batch_size)[0]
plt.figure(figsize=(6, 6))
plt.scatter(x_test_encoded[:,0], x_test_encoded[:,1], c=y_test, cmap='viridis')
plt.colorbar()
plt.show()

分析第一行代码:encoder.predict(x_test, batch_size=batch_size)[0]

a= encoder.predict(x_test, batch_size=batch_size)
len(a)   # 返回结果为3。

先说encoder的定义:

# defining the encoder as a keras model
encoder = Model(x, [z_mean, z_log_var, z], name="encoder")

encoder的定义指出输入为第一个参数x,输出为[z_mean, z_log_var, z],输出3个值,这看起来好像跟前面有不够协调的地方,仔细Recall一下,我们在连接的时候,选用的是编码器的3rd Layer层,并没有选择这个Lambda层。

测试集中所有点到潜在空间及其类的二维投影。图上显示的是二维潜在空间。我们映射出这些生成样本的类别,并根据右侧的图例相应地为它们着色。可以看到同一类样本倾向于整齐的分组在一起,这说明是一个很好的表示。

(13)生成效果图

来自代码注释的翻译:

The only tricky part here is that we have to create a grid over which we interpolate. Which we do by using np.linspace, which enables us to move between min and max value in step_size.

We then just generate a digit for each sample in the for loop and display!

这里唯一棘手的部分是我们需要创建一个网格来进行插值。使用numpy中的np.linspace可以实现在min和max之间按照step_size来移动。然后,我们在for循环中为每个样本生成一个数字并显示!

# display a 2D manifold of the digits
n = 15  # figure with 15x15 digits  15行15列数。
digit_size = 28    # 每个图像大小还是28乘以28个像素。
figure = np.zeros((digit_size * n, digit_size * n))   # 初始化图像。
# linearly spaced coordinates on the unit square were transformed through the inverse CDF (ppf) of the Gaussian
# to produce values of the latent variables z, since the prior of the latent space is Gaussian
grid_x = norm.ppf(np.linspace(0.05, 0.95, n))
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_sample = np.array([[xi, yi]])
        x_decoded = decoder.predict(z_sample)
        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()

 from scipy.stats import norm导入了norm的包,然后norm.ppf()的作用就是找到正态分布概率密度函数为某个值(概率为某个值)时,对应x轴的点的位置(是求积分的反向操作)。

numpy.linspace(start, stop, num=50, endpoint=True, retstep=False, dtype=None)在指定的间隔范围内返回均匀间隔的数字。在[start, stop]范围内计算,返回num个(默认为50)均匀间隔的样本。

8、为什么要尝试使用GAN

为了理解面临的挑战:书中举了个一维双峰分布的例子:

在我们不知道底层的模型长什么样子的情况下,从这个真实的分布中抽取了一堆样本,来推断这些样本的分布情况,一般我们假设真实分布是简单的高斯分布,只需要计算均值和方差即可。这个假设与实际情况不符,甚至说是错误的假设。如果继续使用最大似然估计来估计分布,则为单峰分布——这其实就是VAE在做的事情。此时我们会估计出一个双峰的平均值的正态分布,如图,灰色双峰是真实的,估计分布是中间单峰高斯分布,称为点估计。如下文中将要提到的“安全中间地带”。

好像看起来也不错嘛,起码是个平均水平,但是我们需要的是鉴别力,识别力,特征抽取和概括的能力。几乎完全消失,这还只是一维的,我们常用的都是在非常高维的空间中确定模型。这个挑战很大。

灰色(理论上)的分布是双峰的,而不是单峰。​​​

如果假设是单峰,就会导致灾难性错误,而且会导致模式崩溃。尤其在使用KL散度时,经常发生,VAE有这个问题,早期的GAN也有这个问题。

VAE使用高斯分布来构建它所见的数据的表示。

但是因为高斯分布有99.7%的概率质量落在中间的3个标准差内,VAE也会选择“安全中间地带”。VAE在某种程度上是在试图直接提出基于高斯分布的基础模型,但是现实很复杂,所以VAE不能像GAN那样进行扩展——GAN可以提取“场景”。

VAE生成的假人脸

 

这些由VAE生成的假人脸边缘非常模糊并融合到了背景中。因为CelebA数据集的图像居中且对齐,使眼睛和嘴巴周围的特征保持一致,但是背景往往会有所不同。VAE选择稳妥的方法:通过选择“安全”像素值使背景模糊,这样可以最大限度地减少损失,但不能生成质量良好的图像。

上面人脸的案例来自Github

GANs对生成任务要比自编码器更擅长。

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值