• 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 名人数据集的使用

  • 相关阅读:
    UVALive 6584 Escape (Regionals 2013 >> Europe
    莫比乌斯反演
    POJ 3986 Math teacher's homework
    ACM一些题目
    重探 DFT
    GDSOI2015 task4 ACU
    GDSOI2015 task2 覆盖半径
    USACO 2005 January Gold The Wedding Juicer
    CQOI2015 选数
    计算圆的包含(两两圆不相交)
  • 原文地址:https://www.cnblogs.com/nxf-rabbit75/p/10013568.html
Copyright © 2020-2023  润新知