深度学习框架之Keras感知:快速搭建生成对抗网络(GAN)生成自己想要的图片

1. 写在前面

如果是刚入深度学习的新手小白,可能有着只学习了一点深度学习的理论,也见识到了各种神经网络的强大而不能立马实现的烦恼,想学习TensorFlow,pytorch等出色强大的深度学习框架,又看到那代码晦涩难懂而有些想知难而退,这时候,我觉得有必要掌握一下Keras了,这是个啥? Keras是高级神经网络API,因为Keras短小精悍,非常适合快速原型制作和神经网络的搭建。在很短的时间内,就能够建立一个模型,以实现出色的结果,让神经网络的搭建像积木一样简单,更重要的一点学习深度学习网络,能快速实现,有满满的成就感,不仅可以增加对知识的理解,更可以给自己提供源源不断的学习动力。

基于这些,想把自己学习Keras的经历整理一下,因为我也是小白学起,正好边学边整理。 最近又正好看了《将夜》,发现昊天世界的修炼等级名称比较有趣(初识,感知,不惑,洞玄,知命),所以为了增加趣味性,把Keras学习系列的名字和修炼级别关联起来了,因为我们学习本身就是一场修行。

深度学习框架经过更新很快,TensorFlow,Pytorch等传播盛行,但短小精悍的Keras在未来仍会占据一席之地,并长期占据下去

如果学习了深度学习框架之Keras初识:像搭积木般的玩转神经网络,那么恭喜你进入了感知境, 感知境中我们要完成下面的任务:

  • 用Keras搭建谷歌的Wide&Deep model(DNN)模型完成一个分类任务
  • 搭建lenet5,alexnet,vgg16(CNN)玩转手写数字识别
  • 搭建LSTM(RNN)进行唐诗的生成
  • 搭建LSTM+attention完成机器翻译的小任务
  • 搭建GAN(生成对抗网络)生成我们想要的图片

五篇文章,五个项目,就可以用Keras玩转DNN, CNN, RNN, GAN。 今天是感知的第二篇文章,用Keras搭建生成对抗网络生成自己想要的图片,在前面那篇文章中,我们用CNN进行了手写数字识别的分类,在这篇文章,我们会用Keras的Sequential方式建立DCGAN网络生成手写数字的数据集,然后再用Keras的Model方式建立DCGAN网络生成彩色的青蛙图片,这样也顺便复习一下初识境的知识。 但是在这之前,我们得需要学习一下GAN的工作原理,当然这里我也会再简单解释一遍,如果想知道GAN的详细细节,可以先看一下我们真的了解生成式对抗网络GAN的工作原理吗?(白话+数学公式推导)这篇文章,算是先修知识吧, 好了,话不多说,现在开始。

知识框架

  • 简单说一下GAN的工作原理(还是以生成手写数字识别为例)和DCGAN
  • Keras搭建DCGAN网络生成手写数字图片(一维黑白)
  • Keras搭建DCGAN网络生成青蛙图片(彩色三维)

OK, let’s go!

2. GAN的工作原理

GAN 的基本原理就在于两个网络:G(Generator)和D(Discriminator),分别是生成器和判别器。

生成器网络以一个随机向量作为输入,并将其解码生成为一张图像,而判别器一张真实或者合成的图像作为输入,并预测该图像是来自于真实数据还是合成的图像。

在训练过程中,生成网络G的目标就是尽量生成真实的图片去欺骗判别网络D。而D的目标就是尽量把G生成的图片和真实的图片分别开来。这样,G和D构成了一个动态的“博弈过程”。在理想状态下,博弈的结果就是G可以生成足以以假乱真的图片G(z),而此时的 D 难以判定生成的图像到底是真是假,最后得到D(G(z)) = 0.5的结果。这块的理解跟博弈论中零和博弈非常类似,可以说 GAN 借鉴了博弈论中相关的思想和方法。

还是引用我上篇文章的手写数字识别的例子,这样下面编程实现的时候也更容易理解些,假设我想生成手写数字,我们应该怎么办呢?

准备工作

  1. 首先, 我们要准备1000张真实的手写数字图片,也就是从手写数字数据集中选出1000张来。

    然后,我要搭建一个生成器(就理解成一个神经网络就可以),这个生成器的任务就是我输入一个向量,你输出给我一张图片即可。 再搭建一个判别器(也是一个神经网络),这个判别器的任务就是我给你一张我生成器的图片和一张真实的图片,你给我做一个二分类问题就可以了。

  2. 然后我们开始训练两个网络(这是重点,看看究竟怎么训练让它们变强的)

    首先,我先让生成器V1随机生成1000张图片(不管图片内容,随机生成即可)并给这些图片打上0标签,然后我把1000张真实的图片打上1标签和上面1000张混合组成一个训练集,去训练我的判别器V1,由于这两种图片相差很大,判别器V1很快就能够看出来哪些是真正的手写数字图片,哪些生成器V1伪造的假图片。

    然后,我们得训练生成器V1,让它提高一下伪造图片的技能(这样一下子就露馅了可不好),怎么做呢? 这时候我们冻结住判别器V1的参数不让他训练,只训练生成器V1的参数(先理解成生成器和判别器是可以搭建到一块的),训练的方式就是不断的调整生成器V1的参数,让判别器V1生成1(目前不是手写数字识别生成的是0),这样训练完了之后,就说明生成器V1生成的图片判别器V1已经判别成真实的手写数字图片了,这样生成器V1的伪造水平提高,我们赋予它一个新的名字生成器V2

    接下来,我们得提高判别器V1的判别水平了,因为它已经无法判别出生成器V2生成的图片和真实的图片的区别。这时候,我们冻结住生成器V2的参数不进行训练,只训练判别器V1的参数,方式就是生成器V2生成的图片为0, 真实图片为1,混合作为训练集让判别器V1做二分类的问题进行参数调整,这样,当准确率很高的时候,判别器V1的判别能力提高了,能够区分出生成器V2生成的图片和真实图片了。 我们把这个判别器赋予新的名字:判别器V2。

    这样我们就完成了一轮生成器和判别器的训练,两者都升级到了V2水平,接下来和上面的思想一样,冻结判别器V2的参数,以判别器V2输出1为目标去调生成器V2的参数,直到这个目标达成,就说明生成器V2的伪造水平进一步提高,判别器V2不行了,接下来,让生成器V3生成图片标0,真实图片标1再次训练判别器V2,这样就完成了两者的又一轮升级。

    重复上面的步骤,直到生成器能够达到以假乱真的地步。

这样我们的目标就达成了,当缺少手写数字训练样本的时候,就可以利用这里锻炼的生成器进行图片生成供我们使用。当然换成别的图片一个道理。

好了,下面就可以根据上面说的过程,我们看看用Keras怎么实现DCGAN模型,来完成这个任务,DCGAN的原始论文为 “UNSUPERVISED REPRESENTATION LEARNING WITH DEEP CONVOLUTIONAL GENERATIVE ADVERSARIAL NETWORKS”,所谓DCGAN,顾名思义就是生成器和判别器都是深度卷积神经网络的GAN。

3. Keras搭建DCGAN网络生成手写数字图片

搭建一个稳健的DCGAN主要在于:

  • 所有的pooling层使用步幅卷积(判别网络)和微步幅度卷积(生成网络)进行替换。
  • 在生成网络和判别网络上使用批处理规范化。
  • 对于更深的架构移除全连接隐藏层。
  • 在判别网络的所有层上使用LeakyReLU激活函数。
  • 在生成网络的所有层上使用ReLU激活函数

好了,下面我们就搭建深度卷积生成对抗网络完成任务, 我们边搭建边回忆上面的过程哈:

  • 导入包

    # 导入包的时候先从必备包开始
    import numpy as np
    import pandas as pd
    import matplotlib.pyplot as plt
    
    # Keras的包, 搭建神经网络用的包
    from keras.models import Sequential
    from keras.layers import Dense, Activation, Flatten, Reshape
    from keras.layers import Conv2D, Conv2DTranspose, UpSampling2D
    from keras.layers import LeakyReLU, Dropout
    from keras.layers import BatchNormalization
    from keras.optimizers import RMSprop
    
    # 设置相关参数
    latent_dim = 100  # 这个是那个生成器接收的向量的维度,这个向量经过生成器后产生手写数字图片
    
    # 手写数字图片的大小和维度
    img_rows = 28
    img_cols = 28
    channel = 1
    
  • 下面我们用Keras建立生成器G(Sequential方法)

    dim = 7
    depth = 64 + 64 + 64 + 64
    dropout = 0.4
    
    # 搭积木生成生成器
    G = Sequential()
    # In: 100   out: dim x dim x depth
    G.add(Dense(dim*dim*depth, input_dim=100))
    G.add(BatchNormalization(momentum=0.9))
    G.add(Activation('relu'))
    G.add(Reshape((dim, dim, depth))
    G.add(Dropout(dropout))
    
    # In: dim x dim x depth    out: 2*dim x 2*dim x depth/2
    G.add(UpSampling2D())   # 上采样函数(2,2)  x表示放大倍数,这里取2代表原来的一行变成了两行(也就是每一行和每一列复制了一下,使得字迹加粗)
    G.add(Conv2DTranspose(int(depth/2), 5, padding='same'))   # 卷积的逆操作
    G.add(BatchNormalization(momentum=0.9)
    G.add(Activation('relu'))
    
    # In: 2*dim x 2*dim x depth/2   Out:4*dim * 4*dim * depth/4
    G.add(UpSampling2D())
    G.add(Conv2DTranspose(int(depth/4), 5, padding='same'))
    G.add(BatchNormalization(momentum=0.9))
    G.add(Activation('relu'))
    
    # In: 4*dim * 4*dim * depth/4   Out: 4*dim * 4*dim * depth/8
    G.add(Conv2DTranspose(int(depth/8), 5, padding='same'))
    G.add(BatchNormalization(momentum=0.9))
    G.add(Activation('relu'))
    
    # Out: 28 x 28 x 1 grayscale image [0.0,1.0] per pix
    G.add(Conv2DTranspose(1, 5, padding='same'))
    G.add(Activation('sigmoid'))
    #G.summary()
    
    

    通过这样的一个生成器,我们输入100维的向量,就会得到一个28 * 28 *1的矩阵,和我们的图片一样大, 这个地方一定要检查好输出维度,因为建立好判别器之后,要把这两个拼起来,所以这里的输出维度一定要和判别器的输入维度统一起来

  • 下面我们用Keras建立判别器D(Sequential方法)

    depth = 64
    dropout = 0.4
    
    D = Sequential()
    # In: 28 x 28 x 1, depth = 1   Out: 14 x 14 x 1, depth=64
    input_shape = (img_rows, img_cols, channel)
    D.add(Conv2D(depth*1, 5, strides=2, input_shape=input_shape, padding='same'))
    D.add(LeakyReLU(alpha=0.2))
    D.add(Dropout(dropout))
    
    # In: 14 * 14 * 1, depth=64, Out:7*7*1, depth=64*2
    D.add(Conv2D(depth*2, 5, strides=2, padding='same'))
    D.add(LeakyReLU(alpha=0.2))
    D.add(Dropout(dropout))
    
    # In: 7 * 7 * 1, depth=128, Out:4*4*1, depth=64*4
    D.add(Conv2D(depth*4, 5, strides=2, padding='same'))
    D.add(LeakyReLU(alpha=0.2))
    D.add(Dropout(dropout))
    
    # In: 4 * 4 * 1, depth=64*4, Out:4*4*1, depth=64*8
    D.add(Conv2D(depth*8, 5, strides=1, padding='same'))
    D.add(LeakyReLU(alpha=0.2))
    D.add(Dropout(dropout))
    
    # Out: 1-dim probability
    D.add(Flatten())
    D.add(Dense(1))
    D.add(Activation('sigmoid'))
    #D.summary()
    
    discriminator_optimizer = RMSprop(lr=0.0002, decay=1e-8)
    D.compile(optimizer=discriminator_optimizer,
                      loss='binary_crossentropy')
    
  • 下面把生成器和判别器接在一块,得到DCGAN模型
    注意,判别器的参数禁止训练

    # 将判别器参数设置为不可训练
    D.trainable = False
    
    # 搭建对抗网络, 接起来即可
    gan = Sequential()
    gan.add(G)
    gan.add(D)
    gan_optimizer = RMSprop(lr=0.0004, 
                                       clipvalue=1.0, 
                                       decay=1e-8)
    gan.compile(optimizer=gan_optimizer, loss='binary_crossentropy')
    
  • 下面是画图像的一个函数,这个不是重点,是为了看一下效果

    def plot_images(save2file=False, fake=True, samples=16, noise=None, step=0):
        filename = 'mnist.png'
        if fake:
            if noise is None:
                noise = np.random.uniform(-1.0, 1.0, size=[samples, 100])
            else:
                filename = "mnist_%d.png" % step
            images = G.predict(noise)
        else:
            i = np.random.randint(0, train_x.shape[0], samples)
            images = train_x[i, :, :, :]
    
        plt.figure(figsize=(10,10))
        for i in range(images.shape[0]):
            plt.subplot(4, 4, i+1)
            image = images[i, :, :, :]
            image = np.reshape(image, [img_rows, img_cols])
            plt.imshow(image, cmap='gray')
            plt.axis('off')
        plt.tight_layout()
        if save2file:
            plt.savefig(filename)
            plt.close('all')
        else:
            plt.show()
    
  • 导入数据集,然后进行训练

    # 导入数据集
    path = './dataset/mnist.npz'  # 提前下好
    f = np.load(path)
    
    # 重置形状,然后归一化一下
    train_x = f['x_train']
    train_x = train_x.reshape(train_x.shape[0], 28, 28, 1).astype('float32') / 255.
    
    # 下面是训练部分,和理论部分的过程基本一致
    epoches = 10000
    batch_size = 256
    
    for i in range(epoches):
    	# 我们从真实图片中得到256张图片
    	images_train = train_x[np.random.randint(0, train_x.shape[0], size=batch_size), :, :, :]
    
    	# 然后我们随机生成一些噪声向量,通过生成器生成256张图片
    	noise = np.random.uniform(-1.0, 1.0, size=[batch_size, 100])
    	images_fake = G.predict(noise)
    
    	# 两者合并成一个训练集,并打上标签
    	x = np.concatenate((images_train, images_fake))
    	y = np.ones([2*batch_size, 1])
    	y[batch_size:, :]) = 0
    
    	# 下面就是让判别器进行训练分类
    	d_loss = D.train_on_batch(x, y)
    
    	# 下面就是训练生成器,还记得吗,以假乱真式训练
    	noise = np.random.uniform(-1.0, 1.0, size=[batch_size, 100])
    	y = np.ones([batch_size, 1])   # 打上1标签
    	# 训练DSGAN
    	a_loss = gan.train_on_batch(noise, y)
    
    	# 下面是画图,看看训练的效果
    	noise_input = np.random.uniform(-1.0, 1.0, size=[16, 100])
    	# 每500次绘制一遍图像,并保存
    	if i % 500 == 0:
         	G.save_weights('./weights/Gmnist_weights.h5')
            print('discriminator loss:', d_loss)
            print('adversarial loss:', a_loss)
            plot_images(save2file=True, samples=noise_input.shape[0], noise=noise_input, step=i)
    

这样,就大功告成了, 让它自己训练吧, 结果如下(训练到9500代时的结果和初始的结果对比):
在这里插入图片描述
脚下留心:

上面保存模型这个地方,我用的是model.save_weight这个函数,这个的作用是只保留了最后训练的参数,释放了计算图,这样,如果想下一次导入模型用的时候,就不能单纯的使用load_model函数导入了。 这时候,得先把模型建立起来之后,然后用load_weights给模型赋予参数。


详细情况可以看这篇博客keras保存模型中的save()和save_weights()

4. Keras搭建DCGAN网络生成青蛙图片

在这里,我们依然是搭建DCGAN网络,只不过我们换Model的方式进行搭建,这两种搭建方式要熟记于心才可以。 并且这里的网络也尝试换一下,毕竟都用一样的改改参数没啥意思, Don’t repeat yourself! 但是思想还是一样的思想。

  • 导入相关包

    import numpy as np
    
    from keras.layers import Conv2D, Dense, LeakyReLU, Dropout, Input
    from keras.layers import Reshape, Conv2DTranspose, Flatten
    from keras.layers import BatchNormalization
    from keras.models import Model
    from keras import optimizers
    import keras
    
    import warnings
    warning.filterwarnings('ignore')
    
    """设置相关参数"""
    # 潜变量维度
    latent_dim = 100
    # 输入像素维度
    height = 32
    width = 32
    channels = 3
    
  • 搭建生成器网络(Model方式)

    generator_input = Input(shape=(latent_dim,))
    x = Dense(128*16*16)(generator_input)
    x = LeakyReLU()(x)
    x = Reshape((16, 16, 128))(x)
    
    # IN: 16*16*128  OUT: 16*16*256
    x = Conv2D(256, 5, padding='same')(x)
    x = LeakyReLU()(x)
    
    # IN: 16*16*256  OUT: 32*32*256
    x = Conv2DTranspose(256, 4, strides=2, padding='same')(x)
    x = LeakyReLU()(x)
    
    # 通过反卷积操作,把维度变得和图片一样大了,就别用反卷积了
    x = Conv2D(256, 5, padding='same')(x)
    x = LeakyReLU()(x)
    x = Conv2D(256, 5, padding='same')(x)
    x = LeakyReLU()(x)
    
    # 把通道变回来
    x = Conv2D(channels, 7, activation='tanh', padding='same')(x)
    
    generator = Model(generator_input, x)
    #generator.summary()
    

    这里可以简单总结一下生成器大体上在做一个什么事情, 我们有一个一维的向量,通过生成器,可以获得一个和图片大小的一个矩阵, 所以我们生成器做的就是通过卷积这些操作进行一系列变换, 首先一般会先通过一个全连接层,然后通过Reshape层之后,把向量转成三维的形式,这时候一般前两维会比图片的维度小,但成倍数关系。 这样,后面我们就可以通过Conv2DTranspose这个卷积的逆操作设定步长进行前两维的维度提升,或者用UpSampling2D()也能实现这个效果,然后就是增加或者减少通道数,然后卷积的一些层,进行最终的变换,这个写多了,就慢慢会有感觉,但一定要保证输出和真实图片的维度要一样。这个层不用complie, 因为要后面和判别器连起来一块训练。

  • 搭建判别器,判别器就真的是一个卷积神经网络了

    discriminator_input = Input(shape=(height, width, channels))
    # IN:32*32*3   OUT: 30*30*128
    x = Conv2D(128, 3)(discriminator_input)
    x = LeakyReLU()(x)
    
    # IN: 30*30*128  OUT:14*14*128
    x = Conv2D(128, 4, strides=2)(x)
    x = LeakyReLU()(x)
    
    # IN:14*14*128   OUT:6*6*128
    x = Conv2D(128, 4, strides=2)(x)
    x = LeakyReLU()(x)
    
    # IN:6*6*128  OUT:2*2*128
    x = Conv2D(128, 4, strides=2)(x)
    x = LeakyReLU()(x)
    
    # 展开成512个神经元
    x = Flatten()(x)
    x = Dropout(0.4)(x)
    x = Dense(1, activation='sigmoid')(x)
    
    discriminator = Model(discriminator_input, x)
    #discriminator.summary()
    
    discriminator_optimizer = optimizers.RMSprop(lr=0.0008, 
                                                 clipvalue=1.0, 
                                                 decay=1e-8)
    
    discriminator.compile(optimizer=discriminator_optimizer,
                          loss='binary_crossentropy')
    

    搭建判别器,就类似搭建普通的卷积神经网络并且只需要做二分类, 这个就需要complie了。

  • 将上面两个连起来组成生成对抗网络

    # 将判别器参数设置为不可训练
    discriminator.trainable = False
    
    gan_input = Input(shape=(latent_dim,))
    gan_output = discriminator(generator(gan_input))
    # 搭建对抗网络
    gan = Model(gan_input, gan_output)
    gan_optimizer = optimizers.RMSprop(lr=0.0004, 
                                       clipvalue=1.0, 
                                       decay=1e-8)
    gan.compile(optimizer=gan_optimizer, loss='binary_crossentropy')
    
  • 下面导入数据集进行训练,数据集选用的是Keras自带的CIFAR-10数据集

    import os
    from keras.preprocessing import image
    
    # 加载数据集(手写数字的数据集也可以这样加载)
    (x_train, y_train), (_,_) = keras.datasets.cifar10.load_data()
    # 指定青蛙图像(编号为6)
    x_train = x_train[y_train.flatten() == 6]
    x_train = x_train.reshape((x_train.shape[0],) +(height, width, channels)).astype('float32') / 255.
    
    # 下面开始训练
    iterations = 10000
    batch_size = 64
    save_dir = './image'
    
    start = 0
    for step in range(iterations):
    	
    	# 先通过噪声生成64张伪造图片
    	noise = np.random.normal(size=(batch_size, latent_dim))
    	images_fake = generator.predict(noise)
    
    	# 从真实图片中抽64张
    	end = start+batch_size
    	images_train = x_train[start:end]
    
    	# 两者混合作为训练集,并打上标签
    	x = np.concatenate([images_fake, images_train])
    	y = np.concatenate([np.zeors((batch_size, 1)), np.ones((batch_size, 1))])
    
    	# 向标签中添加噪声
    	y += 0.05 * np.random.random(y.shape)
    
    	# 训练判别器
    	d_loss = discriminator.train_on_batch(x, y)
    
    	# 训练生成对抗网络
    	noise = np.random.normal(size=(batch_size, latent_dim))
    	labels = np.ones((batch_size, 1))
    
    	# 通过gan模型来训练生成器模型,冻结判别器模型权重
    	a_loss = gan.train_on_batch(noise, labels)
    	
    	start += batch_size
    	if start > len(x_train) - batch_size:
    		start = 0
    	
    	# 每500步绘图并保存
    	if step % 500 == 0:
            generator.save_weights('./weights/G_weights.h5')
            print('discriminator loss:', d_loss)
            print('adversarial loss:', a_loss)
            img = image.array_to_img(images_fake[0] * 255., scale=False)
            img.save(os.path.join(save_dir, 'generated_frog' + str(step) + '.png'))
            img = image.array_to_img(images_train[0] * 255., scale=False)
            img.save(os.path.join(save_dir, 'real_frog' + str(step) + '.png'))
    

这样,我们就完成了一个生成3维图片的任务。训练结果:
在这里插入图片描述
受限于CIFAR-10数据本身的低像素性,DCGAN生成出来的图像虽然也很模糊,但基本上足以达到以假乱真的水平。上图图片中,每一列有两张是生成样本,有一张是真实样本,按列第2、1、3和2张图片是真实样本,其余都是DCGAN伪造出来的青蛙图片。

脚下留心:

关于训练,这个训练需要挺长时间的,并且我试图把优化方法RMSprop改成Adam,发现效果变差了,并且这两个项目里面的优化方法都是RMSprop,你也可以试试其他的优化方法或者学习率什么的, 但是目前我看到的这种GAN里面的优化方法,RMSprop的多。

当然这个也可以用上面的那个手写数字的跑,只不过需要改一些参数,毕竟维度不同,那个效果可能还好些吧。

5. 总结

好了,搭建生成对抗网络的学习就到这里吧,通过这一节课,相信可以使用Keras搭建一个不错的GAN来生成自己想要的图像了吧。 简单梳理一下这次学习: 首先是介绍了生成对抗网络的工作原理,然后用Keras的Sequential方式搭建了DCGAN完成了个手写数字生成,并在这里面拓展了一点模型保存的知识, 然后用Keras的Model方式搭建了另一个网络生成了一些彩色图片,在这里汇总了一下搭建生成器和判别器的一些思想。

这样两个项目下来,估计对GAN也不是那么陌生了吧,感知境的第二篇文章已经完成, 后面还有更加精彩的Keras搭建LSTM, Attention的一些知识, 还得继续修炼, Rush !

参考

发布了85 篇原创文章 · 获赞 107 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/wuzhongqiang/article/details/104563011