1.VAE和GAN
- 变分自编码器(VAE,variatinal autoencoder)
- 生成式对抗网络(GAN,generative adversarial network)
两者不仅适用于图像,还可以探索声音、音乐甚至文本的潜在空间;
- VAE非常适合用于学习具有良好结构的潜在空间,其中特定方向表示数据中有意义的变化轴;
- GAN生成的图像可能非常逼真,但它的潜在空间可能没有良好结构,也没有足够的连续型。
自编码,简单来说就是把输入数据进行一个压缩和解压缩的过程。 原来有很多 Feature,压缩成几个来代表原来的数据,解压之后恢复成原来的维度,再和原数据进行比较。它是一种非监督算法,只需要输入数据,解压缩之后的结果与原数据本身进行比较。
在实践中,这种经典的自编码器不会得到特别有用或具有良好结构的潜在空间。它们也没有对数据做多少压缩。因此,它们已经基本上过时了(Keras 0.x版本还有AutoEncoder这个层,后来直接都删了)。但是,VAE向自编码器添加了一点统计魔法,迫使其学习连续的、高度结构化的潜在空间。这使得VAE已成为图像生成的强大工具。变分编码器和自动编码器的区别就在于,传统自动编码器的隐变量z的分布是不知道的,因此我们无法采样得到新的z,也就无法通过解码器得到新的x。下面我们来变分,我们现在不要从x中直接得到z,而是得到z的均值和方差,然后再迫使它逼近正态分布的均值和方差,则网络变成下面的样子:
然而上面这个网络最大的问题是,它是断开的。前半截是从数据集估计z的分布,后半截是从一个z的样本重构输入。最关键的采样这一步,恰好不是一个我们传统意义上的操作。这个网络没法求导,因为梯度传到f(z)以后没办法往前走了。为了使得整个网络得以训练,使用一种叫reparemerization的trick,使得网络对均值和方差可导,把网络连起来。这个trick的idea见下图:
实际上,这是将原来的单输入模型改为二输入模型了。因为服从标准正态分布,所以它乘以估计的方差加上估计的均值,效果跟上上图直接从高斯分布里抽样本结果是一样的。这样,梯度就可以通上图红线的方向回传,整个网络就变的可训练了。
VAE的工作原理:
(1)一个编码器模块将输入样本input_img转换为表示潜在空间中的两个参数z_mean和z_log_variance;
(2)我们假定潜在正态分布能够生成输入图像,并从这个分布中随机采样一个点:z=z_mean + exp(z_log_variance)*epsilon,其中epsilon是取值很小的随机张量;
(3)一个解码器模块将潜在空间的这个点映射回原始输入图像。
因为epsilon是随机的,所以这个过程可以确保,与input_img编码的潜在位置(即z-mean)靠近的每个点都能被解码为与input_img类似的图像,从而迫使潜在空间能够连续地有意义。潜在空间中任意两个相邻的点都会被解码为高度相似的图像。连续性以及潜在空间的低维度,将迫使潜在空间中的每个方向都表示数据中一个有意义的变化轴,这使得潜在空间具有非常良好的结构,因此非常适合通过概率向量来进行操作。
VAE的参数通过两个损失函数来进行训练:一个是重构损失(reconstruction loss),它迫使解码后的样本匹配初始输入;另一个是正则化损失(regularization loss),它有助于学习具有良好结构的潜在空间,并可以降低训练数据上的过拟合。
实现代码如下:
编码自编码器是更现代和有趣的一种自动编码器,它为码字施加约束,使得编码器学习到输入数据的隐变量模型。 |
|
import keras
from keras import layers
from keras import backend as K
from keras.models import Model
from keras.layers import Input,Dense
import numpy as np
img_shape = (28,28,1)
latent_dim = 2 #潜在空间的维度:一个二维平面
input_img = keras.Input(shape=img_shape)
encoded = layers.Conv2D(32,3,padding='same',activation='relu')(input_img)
encoded = layers.Conv2D(64,3,padding='same',activation='relu',strides=(2,2))(encoded)
encoded = layers.Conv2D(64,3,padding='same',activation='relu')(encoded)
encoded = layers.Conv2D(64,3,padding='same',activation='relu')(encoded)
shape_before_flattening = K.int_shape(encoded)
shape_before_flattening
|
卷积层的输入必须是3维的(长,宽,1或者3) keras不需要输入batch的大小,fit时候再设置 shape_before_flattening (None, 14, 14, 64) |
encoded = layers.Flatten()(encoded)
encoded = layers.Dense(32,activation='relu')(encoded)
#输入图像最终被编码为这两个参数
z_mean = layers.Dense(latent_dim)(encoded)
z_log_var = layers.Dense(latent_dim)(encoded)
#编码器 输入图片-->得到二维特征
encoder = Model(input_img,z_mean)
|
z_mean ---> <tf.Tensor 'dense_5/BiasAdd:0' shape=(?, 2) dtype=float32> K.shape(z_mean) --->
|
#潜在空间采样的函数
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,output_shape=(latent_dim,))([z_mean,z_log_var])
|
在keras中,任何对象都应该是一个层,如果代码不是内置层的一部分, 我们应该将其包装到一个Lambda层(或自定义层)中 Keras的Lambda层以一个张量函数为参数,对输入的数据按照张量函数的要求做映射。 本质上就是Keras layer中.call()的快捷方式。先定义运算逻辑 K.int_shape(z) ---> (None,2) None应该是batch_size |
#VAE解码器网络,将潜在空间点映射为图像
decoder_input = layers.Input(K.int_shape(z)[1:]) #将z调整为图像大小,需要将z输入到这里
#对输入进行上采样
decoded = layers.Dense(np.prod(shape_before_flattening[1:]),activation='relu')(decoder_input)
#将z转换为特征图,使其形状与编码器模型最后一个Flatten层之前的特征图的形状相同
decoded = layers.Reshape(shape_before_flattening[1:])(decoded)
#使用一个Conv2DTranspose层和一个Conv2D层,将z解码为与原始输入图像具有相同尺寸的特征图
decoded = layers.Conv2DTranspose(32,3,padding='same',activation='relu',strides=(2,2))(decoded)
decoder_output = layers.Conv2D(1,3,padding='same',activation='sigmoid')(decoded)
#将解码器模型实例化,它将decoder_input转换为解码后的图像
decoder = Model(decoder_input,decoder_output)
#将这个实例应用于z,以得到解码后的z
z_decoded = decoder(z)
|
|
#用于计算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)
#编写一个call方法,来实现自定义层
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(input,target),VAE的双重损失不符合这种形式。 因此,损失的设置方法为:编写一个自定义层,并在其内部使用内置的add_loss层方法 来创建一个你想要的损失 |
|
|
#训练VAE
vae = Model(input_img,y)
vae.compile(optimizer='rmsprop',loss=None)
vae.summary()
|
|
from keras.datasets import mnist
(x_train,_),(x_test,y_test) = mnist.load_data()
x_train = x_train[:600]
x_test = x_test[:100]
x_train = x_train.astype('float32')/255.
print('x_train.shape',x_train.shape)
x_train =x_train.reshape(x_train.shape+(1,))
print('x_train.shape',x_train.shape)
x_test = x_test.astype('float32')/255.
print('x_test.shape',x_test.shape)
x_test = x_test.reshape(x_test.shape+(1,))
print('x_test.shape',x_test.shape)
|
x_train.shape (600, 28, 28)
x_train.shape (600, 28, 28, 1)
x_test.shape (100, 28, 28)
x_test.shape (100, 28, 28, 1)
|
vae.fit(x_train,None,
shuffle=True,
epochs=1,
batch_size=100,
validation_data = (x_test,None)
)
|
一旦训练好了这样的模型,我们就可以使用decoder网络将任意潜在空间向量 转换为图像 |
#从二维潜在空间中采样一组点的网络,并将其解码为图像
import matplotlib.pyplot as plt
from scipy.stats import norm
batch_size = 100
n = 15 #我们将显示15*15的数字网格(共225个数字)
digit_size=28
figure = np.zeros((digit_size*n,digit_size*n))
#使用scipy的ppf函数对线性分割的坐标进行变换,以生存潜在变量z的值(因为潜在空间的先验分布是高斯分布)
grid_x = norm.ppf(np.linspace(0.05,0.95,n))
grid_y = norm.ppf(np.linspace(0.05,0.95,n))
print(grid_x)
print(grid_y)
for i,yi in enumerate(grid_x):
for j,xi in enumerate(grid_y):
z_sample = np.array([[xi,yi]])
z_sample = np.tile(z_sample,batch_size).reshape(batch_size,2)#将z多次重复,以构建一个完整的批量
x_decoded = decoder.predict(z_sample,batch_size=batch_size)#将批量解码为数字图像
digit = x_decoded[0].reshape(digit_size,digit_size)#将批量第一个数字形状从28*28*1转变为28*28
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()
|
因为训练时候就用了600个数据,所以效果很差....电脑实在带不动,┭┮﹏┭┮ 以后有服务器再试试,7777777 |
小结: 用深度学习进行图像生成,就是通过对潜在空间进行学习来实现的,这个潜在空间能够捕捉到关于图像数据集的统计信息。 通过对潜在空间中的点进行采样和编码,我们可以生成前所未见的图像。
网上的代码大部分都是关于mnist数据集的,直接load_dataset就完事了,我找到了名人头像的数据集celebrity_data,用这个数据集做vae更有趣一点。
import keras
from keras import layers
from keras import backend as K
from keras.models import Model
import numpy as np
import skimage
import glob
from skimage import io
import os
import imageio
|
|
train_imgs = glob.glob('./celebrity_data/train/*.jpg')
np.random.shuffle(train_imgs)
test_imgs = glob.glob('./celebrity_data/test/*.jpg')
np.random.shuffle(train_imgs)
nxf_image = io.imread(test_imgs[0])
|
Image读出来的是PIL的类型,而skimage.io读出来的数据是numpy格式的 import Image as img
import os
from matplotlib import pyplot as plot
from skimage import io,transform
#Image和skimage读图片
img_file1 = img.open('./CXR_png/MCUCXR_0042_0.png')
img_file2 = io.imread('./CXR_png/MCUCXR_0042_0.png')
输出可以看出Img读图片的大小是图片的(width, height);而skimage的是(height,width, channel) |
height,width = imageio.imread(train_imgs[0]).shape[:2]
center_height = int((height-width)/2)
img_xdim = 218
img_ydim = 178
z_dim = 512
|
训练集里面的图片都是218*178*3的,训练的时候我也没有改大小,直接放进去训练的 |
def imread(f):
x = imageio.imread(f)
x = x[center_height:center_height+width,:]
x = skimage.transform.resize(x,(img_xdim,img_ydim),mode='constant')
return x.astype(np.float32)/255 * 2 - 1
def train_data_generator(batch_size=32):
X = []
while True:
np.random.shuffle(train_imgs)
for f in train_imgs:
X.append(imread(f))
if len(X) == batch_size:
X = np.array(X)
yield X,None
X = []
|
train_data_generator是训练集图片生成器,每次生成一个图片 |
img_shape = (img_xdim,img_ydim,3)
latent_dim = 2 #潜在空间的维度:一个二维平面
input_img = keras.Input(shape=img_shape)
encoded = layers.Conv2D(32,3,padding='same',activation='relu')(input_img)
encoded = layers.Conv2D(64,3,padding='same',activation='relu',strides=(2,2))(encoded)
encoded = layers.Conv2D(64,3,padding='same',activation='relu')(encoded)
encoded = layers.Conv2D(64,3,padding='same',activation='relu')(encoded)
shape_before_flattening = K.int_shape(encoded)
encoded = layers.Flatten()(encoded)
encoded = layers.Dense(32,activation='relu')(encoded)
#输入图像最终被编码为这两个参数
z_mean = layers.Dense(latent_dim)(encoded)
z_log_var = layers.Dense(latent_dim)(encoded)
encoder = Model(input_img,z_mean)
|
这部分和上面基于minist数据集的encoder部分一样 |
#将图片转换为二维向量
nxf_image = nxf_image.reshape((1,)+nxf_image.shape)
nxf_image_encoder = encoder.predict(nxf_image)
print('nxf_image_encoder',nxf_image_encoder)
|
这里是我在测试encoder,随机输入一张图片,输出了二维的一个值,一个是均值,一个是方差,encoder没有编译, 也没有fit,就相当于将多维图片降维成二维的一组 |
# 潜在空间采样的函数
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,output_shape=(latent_dim,))([z_mean,z_log_var])
#VAE解码器网络,将潜在空间点映射为图像
decoder_input = layers.Input(K.int_shape(z)[1:]) #将z调整为图像大小,需要将z输入到这里
#对输入进行上采样
decoded = layers.Dense(np.prod(shape_before_flattening[1:]),activation='relu')(decoder_input)
#将z转换为特征图,使其形状与编码器模型最后一个Flatten层之前的特征图的形状相同
decoded = layers.Reshape(shape_before_flattening[1:])(decoded)
#使用一个Conv2DTranspose层和一个Conv2D层,将z解码为与原始输入图像具有相同尺寸的特征图
decoded = layers.Conv2DTranspose(32,3,padding='same',activation='relu',strides=(2,2))(decoded)
decoder_output = layers.Conv2D(3,3,padding='same',activation='sigmoid')(decoded)
#将解码器模型实例化,它将decoder_input转换为解码后的图像
decoder = Model(decoder_input,decoder_output)
#将这个实例应用于z,以得到解码后的z
z_decoded = decoder(z)
# decoder.summary()
|
这部分也是一样的,解码操作,随机生成一个点(均值,方差)放入decoder中,看看生成的图片能不能和原来的图片一样 |
# 用于计算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)
# 编写一个call方法,来实现自定义层
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])
# 训练VAE
vae = Model(input_img, y)
vae.compile(optimizer='rmsprop', loss=None)
# vae.summary()
|
VAE的两个损失,由于keras自带的损失函数没有同时有正则损失和重构损失,所以需要自定义一个损失层, 使用call函数来定义该损失层的功能 |
def sample(path):
figure_nxf = np.array(nxf_image_encoder)
nxf_recon = decoder.predict(figure_nxf)[0]
imageio.imwrite(path,nxf_recon)
from keras.callbacks import Callback
class Evaluate(Callback):
def __init__(self):
import os
self.lowest = 1e10
self.losses = []
if not os.path.exists('samples'):
os.mkdir('samples')
def on_epoch_end(self, epoch, logs=None):
path = 'samples/test_%s.png' % epoch
sample(path)
self.losses.append((epoch, logs['loss']))
if logs['loss'] <= self.lowest:
self.lowest = logs['loss']
encoder.save_weights('./best_encoder.weights')
evaluator = Evaluate()
vae.fit_generator(train_data_generator(),
epochs=1,
steps_per_epoch=1,
callbacks=[evaluator])
|
sample函数,我就随机输入两个值(encoder的输出值),看看能不能生成一个相似的图片 |
参考文献:
【2】变分自编码器(Variational Autoencoder, VAE)通俗教程
【5】vae 名人数据集的使用