4.keras实现-->生成式深度学习之用变分自编码器VAE生成图像(mnist数据集和名人头像数据集)

1.VAE和GAN

  1. 变分自编码器(VAE,variatinal autoencoder)   
  2. 生成式对抗网络(GAN,generative adversarial network)

两者不仅适用于图像,还可以探索声音、音乐甚至文本的潜在空间;

  1. VAE非常适合用于学习具有良好结构的潜在空间,其中特定方向表示数据中有意义的变化轴; 
  2. GAN生成的图像可能非常逼真,但它的潜在空间可能没有良好结构,也没有足够的连续型。

 

       自编码,简单来说就是把输入数据进行一个压缩和解压缩的过程。 原来有很多 Feature,压缩成几个来代表原来的数据,解压之后恢复成原来的维度,再和原数据进行比较。它是一种非监督算法,只需要输入数据,解压缩之后的结果与原数据本身进行比较。

  在实践中,这种经典的自编码器不会得到特别有用或具有良好结构的潜在空间。它们也没有对数据做多少压缩。因此,它们已经基本上过时了(Keras 0.x版本还有AutoEncoder这个层,后来直接都删了)。但是,VAE向自编码器添加了一点统计魔法,迫使其学习连续的、高度结构化的潜在空间。这使得VAE已成为图像生成的强大工具。变分编码器和自动编码器的区别就在于,传统自动编码器的隐变量z的分布是不知道的,因此我们无法采样得到新的z,也就无法通过解码器得到新的x。下面我们来变分,我们现在不要从x中直接得到z,而是得到z的均值和方差,然后再迫使它逼近正态分布的均值和方差,则网络变成下面的样子:

       然而上面这个网络最大的问题是,它是断开的。前半截是从数据集估计z的分布,后半截是从一个z的样本重构输入。最关键的采样这一步,恰好不是一个我们传统意义上的操作。这个网络没法求导,因为梯度传到f(z)以后没办法往前走了。为了使得整个网络得以训练,使用一种叫reparemerization的trick,使得网络对均值和方差可导,把网络连起来。这个trick的idea见下图:

       实际上,这是将原来的单输入模型改为二输入模型了。因为varepsilon 服从标准正态分布,所以它乘以估计的方差加上估计的均值,效果跟上上图直接从高斯分布里抽样本结果是一样的。这样,梯度就可以通上图红线的方向回传,整个网络就变的可训练了。

 

 

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) --->
<tf.Tensor 'Shape:0' shape=(2,) dtype=int32>

 

 
 
#潜在空间采样的函数
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

  

 
  • skimage即是Scikit-Image。基于python脚本语言开发的数字图片处理包,比如PIL,Pillow, opencv, scikit-image等。
  • PIL和Pillow只提供最基础的数字图像处理,功能有限;opencv实际上是一个c++库,只是提供了python接口,更新速度非常慢。
  • scikit-image是基于scipy的一款图像处理包,它将图片作为numpy数组进行处理,正好与matlab一样,
  • 因此,我们最终选择scikit-image进行数字图像处理。
 
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的输出值),看看能不能生成一个相似的图片

 


参考文献:

【1】Keras示例程序解析(4):变分编码器VAE

【2】变分自编码器(Variational Autoencoder, VAE)通俗教程

【3】变分自编码器VAE:一步到位的聚类方案

【4】如何使用变分自编码器VAE生成动漫人物形象

【5】vae 名人数据集的使用

原文地址:https://www.cnblogs.com/nxf-rabbit75/p/10013568.html