基于Keras的生成对抗网络(2)——利用Keras搭建DCGAN生成手写体数字

时间:2024-09-29 15:00:17

目录

    • 0、前言
    • 一、DCGAN介绍及其结构
      • 1.1 DCGAN介绍
      • 1.2 DCGAN结构
      • 1.3 DCGAN的优点
      • 1.4 详细介绍
    • 二、函数代码
      • 2.0 训练前的准备
      • 2.1 生成器Generator
      • 2.2 判别器Discriminator
      • 2.3 train函数
    • 三、结果演示
    • 四、完整代码
    • 五、常见问题汇总
    • 六、【拓展】利用DCGAN实现三通道彩色图像生成

0、前言

上一篇介绍了GAN 的基本原理以及相关的概念和代码示例,同时也反映出GAN 的一些缺点,比如说训练不稳定,生成过程不可控,不具备可解释性等。这一篇就来看看GAN 的改进版之一,DCGAN。

一、DCGAN介绍及其结构

1.1 DCGAN介绍

DCGAN的全称是Deep Convolutional Generative Adversarial Networks,翻译为深度卷积对抗生成网络。

它是由A Radford在2015年发表的论文《Unsupervised Representation Learning with Deep Convolutional Generative Adversarial Networks》中提出的。
论文下载链接:/pdf/1511.06434
文章在GAN的基础上提出了全新的DCGAN架构,该网络在训练过程中状态稳定,并且可以有效的实现高质量的图片生成及相关的生成模型应用。由于其具有非常强的实用性,在它之后的大量GAN模型都是基于DCGAN进行的改良版本。

1.2 DCGAN结构

实际上,DCGAN就是在普通GAN的基础上增加卷积神经网络结构。
论文中给出的DCGAN生成器结构如下图所示。其使用反卷积将特征层的高宽不断扩大,整体结构看起来像普通神经网络的逆过程。
在这里插入图片描述
该结构将一个100维均匀分布的z投影到具有许多特征映射的小空间范围卷积表示中,4步微步卷积【作者在文中称为fractionally-strided convolutions】(在一些论文中,这被错误的称为反卷积)然后将其转换成一个64x64x3像素的图像。值得注意的是,这里没有用全连接层和池化层。

1.3 DCGAN的优点

DCGAN 相比于GAN 或者是普通CNN 的改进包含以下几个方面:

(1)使用卷积代替池化层

(2)在生成器和判别器中都添加了批量归一化操作(batch normalization)

(3)去掉了全连接层,使用全局池化层替代

(4)生成器的输出层使用tanh 激活函数,其他层使用RELU

(5)判别器的所有层都是用LeakyReLU 激活函数

1.4 详细介绍

首先第一点是把传统卷积网络中的池化层全部去除,使用卷积层代替。对于判别器,我们使用步长卷积(strided convolution)来代替池化层;对于生成器,我们使用分数步长卷积(fractional-strided convolutions)来代替池化层。
步长卷积示意图如下:
在这里插入图片描述
上图表示了卷积层如何在判别器中进行空间下采样(spatial downsampling),输入数据为5x5的矩阵,使用了3x3的滤波器,步长为2x2,最终输出矩阵为3x3。

分数步长卷积示意图如下:
在这里插入图片描述
上图表示的是卷积层在生成器中进行上采样(spatial upsampling),输入为3x3矩阵,同样使用3x3滤波器,反向步长为2x2,故在每个输入矩阵的点之间填充一个0,最终输出为5x5。

使用上述卷积层代替池化层的目的是为了能够让网络自身去学习空间上采样与下采样,使得判别器和生成器都能够有效具备相应的能力。

第二点设计规则是去除全连接层。全连接层的缺点在于参数过多,此外全连接层也会使网络出现过拟合。

第三点设计规则是使用批归一化(batch normalization)。由于深度学习的神经网络层数很多,每一层都会使得输出数据的分布发生变化,随着层数的增加网络的整体偏差会越来越大。批归一化的目标则是为了解决这一问题,通过对每一层的输入进行归一化处理,能够有效使得数据服从某个固定的数据分布。

最后一点是对于激活函数的设计。激活函数的作用是为了在神经网络中进行非线性变换。

二、函数代码

2.0 训练前的准备

首先确定基础信息:

  • 由于是黑白图像,所以通道数为1,输入图像的尺寸为(28,28,1)
  • 生成器输入为100维随机噪声

2.1 生成器Generator

生成器的结构如上图所示。
对于生成器来说,它的目的是生成假图片,它的输入是正态分布随机数,输出是假图片。
在这里插入图片描述

在DCGAN的生成网络中,输入一个100维的随机噪声,我们将其reshape成7*7*256的特征层,利用四次反卷积(作者称之为fractionally-strided convolutions),其变化如下:
(7*7*256)——>(14*14*128)——>(28*28*64)——>(28*28*32)——>(28*28*1)与训练图片大小一致。

整个过程没有使用全连接,使用转置卷积直接替换上采样和卷积层(某些资料说能达到更好的效果)。激活层之前都使用了BatchNormalization,激活函数除了最后输出层采用tanh,其它都采用relu。

函数代码:

    #定义生成器模型
    #输入:100维向量;输出:28*28*1图像(像素大小为(-1,1)--tanh)
    def build_generator(self):

        model=Sequential()                                                     
        noise=Input(shape=(self.latent_dim,))#输入
        model.add(Dense(256*7*7, activation='relu',input_dim=self.latent_dim))#全连接
        model.add(BatchNormalization(momentum=0.8))
        model.add(Reshape((7,7,256)))
        #第一层转置卷积层
        model.add(Conv2DTranspose(128,kernel_size=3,strides=2,padding="same"))
        model.add(BatchNormalization(momentum=0.8))
        model.add(Activation('relu'))#激活函数
        #第二层转置卷积层
        model.add(Conv2DTranspose(64,kernel_size=3,strides=2,padding="same"))
        model.add(BatchNormalization(momentum=0.8))
        model.add(Activation('relu'))#激活函数
        #第三层转置卷积层
        model.add(Conv2DTranspose(32,kernel_size=3,padding="same"))
        model.add(BatchNormalization(momentum=0.8))
        model.add(Activation('relu'))#激活函数
        #第四层转置卷积层
        model.add(Conv2DTranspose(1,kernel_size=3,padding="same"))
        model.add(Activation('tanh'))#激活函数

        model.summary()#输出模型参数状况
        img = model(noise)
        plot_model(model, to_file='', show_shapes=True, show_layer_names=False, rankdir='TB')#绘制模型
        return Model(noise, img)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

生成器模型如下:
在这里插入图片描述

2.2 判别器Discriminator

对于判别器,它的目的是判断输入图片的真假,它的输入是图片,输出是判断结果。
判断结果处于0-1之间,利用接近1代表判断为真图片,接近0代表判断为假图片。
在这里插入图片描述

判断网络的构建和普通卷积网络差距不大,都是不断的卷积对图片进行下采样,在多次卷积后,最终接一次全连接判断结果。

代码如下:

    #定义判别器模型
    #输入:28*28图像;输出:0-1数字
    def build_discriminator(self):
        model=Sequential()
        img=Input(shape=self.img_shape)
        #卷积层1
        model.add(Conv2D(64,kernel_size=3,strides=2,input_shape=self.img_shape,padding="same"))
        model.add(LeakyReLU(alpha=0.2))#激活函数
        model.add(Dropout(0.25))
        #卷积层2
        model.add(Conv2D(128,kernel_size=3,strides=2,padding="same"))
        model.add(LeakyReLU(alpha=0.2))#激活函数
        model.add(Dropout(0.25))
        #卷积层3
        model.add(Conv2D(256,kernel_size=3,strides=2,padding="same"))
        model.add(LeakyReLU(alpha=0.2))#激活函数
        model.add(Dropout(0.25))
        #卷积层4
        model.add(Conv2D(512,kernel_size=3,strides=1,padding="same"))
        model.add(LeakyReLU(alpha=0.2))#激活函数
        model.add(Dropout(0.25))
        #输出层
        model.add(Flatten())#铺平
        model.add(Dense(1,activation='sigmoid'))

        model.summary()#输出模型参数状况

        validity = model(img)
        plot_model(model, to_file='', show_shapes=True, show_layer_names=False, rankdir='TB')#绘制模型
        return Model(img,validity)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

判别器模型如下:
在这里插入图片描述

2.3 train函数

DCGAN的训练和GAN一样,分为如下几个步骤:

  1. 将mnist数据集中的图像归一化(归一化为[-1,1])
  2. 随机选取batch_size个真实的图像
  3. 随机生成batch_size个100维向量,传入到Generator中生成batch_size个虚假的图像
  4. 真实图片的label为1,虚假图片的label为0,将真实图片和虚假图片当作训练集传入到Discriminator中进行训练。
  5. 将虚假图片的Discriminator预测结果与1的对比作为loss对Generator进行训练(与1对比的意思是,如果Discriminator将虚假图片判断为1,说明这个生成的图片很“真实”)。

train函数代码如下:

#定义训练函数
def train(self,epochs,batch_size=128,sample_interval=50):
    #创建训练数据集
    (X_train, _), (_, _) = mnist.load_data() # 从mnist数据集中获得数据
    X_train =X_train/127.5-1 # 标准化为(-1,1)
    X_train = np.expand_dims(X_train, axis=3) #60000*28*28变为60000*28*28*1;X_train的shape是60000*28*28*1
    
    #创建标签(0或者1)--二分类问题
    valid = np.ones((batch_size, 1))#全1阵--真
    fake = np.zeros((batch_size, 1))#全0阵--假

    for epoch in range(epochs):
        #训练discriminator
        idx=np.random.randint(0,X_train.shape[0],batch_size)#随机选择batch_size个数
        imgs=X_train[idx]#真图像

        noise = np.random.normal(0, 1, (batch_size, self.latent_dim))#生成正态分布噪声
        gen_imgs = self.generator.predict(noise)#假图像

        #训练discriminator
        d_loss_real = self.discriminator.train_on_batch(imgs, valid)#真图像训练
        d_loss_fake = self.discriminator.train_on_batch(gen_imgs, fake)#假图像训练
        d_loss = 0.5 * np.add(d_loss_real, d_loss_fake)#数据求和

        #训练生成器
        g_loss=self.combined.train_on_batch(noise,valid)
        print("steps:%d [D loss: %f, acc.: %.2f%%] [G loss: %f]"% (epoch, d_loss[0], 100*d_loss[1],g_loss))

        #迭代50次打印一次
        if epoch % sample_interval == 0:
            self.sample_images(epoch)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

三、结果演示

在这里插入图片描述

四、完整代码

完整代码如下:

from keras.datasets import mnist
from keras.models import Sequential,Model
from keras.layers import Input, Dense, Reshape, Flatten
from keras.layers import BatchNormalization, Activation,Dropout
from keras.layers import LeakyReLU,UpSampling2D,Conv2D,ZeroPadding2D,Conv2DTranspose
from keras.optimizers import Adam
from keras.utils.vis_utils import plot_model
import matplotlib.pyplot as plt
import numpy as np
import os
class DCGAN():
    def __init__(self):
        #28,28,1
        self.img_shape = (28,28,1)
        self.latent_dim=100 #输入维度--100
        optimizer = Adam(0.0002, 0.5)#定义Adam优化器
    
        #判别器
        self.discriminator=self.build_discriminator()
        self.discriminator.compile(loss='binary_crossentropy', optimizer=optimizer, metrics=['accuracy'])#定义loss函数和优化器
        #生成器
        self.generator=self.build_generator()
        gan_input = Input(shape=(self.latent_dim,))
        img = self.generator(gan_input)

        #训练生成器
        # 在训练generator的时候不训练discriminator
        self.discriminator.trainable = False#冻结discriminator
        validity = self.discriminator(img)# 对生成的假图片进行预测
        self.combined = Model(gan_input, validity)#在Model中discriminator已经被冻结,仅剩下generator在运行了
        self.combined.compile(loss='binary_crossentropy', optimizer=optimizer)#定义loss函数和优化器


    #定义生成器模型
    #输入:100维向量;输出:28*28*1图像(像素大小为(-1,1)--tanh)
    def build_generator(self):

        model=Sequential()                                                     
        noise=Input(shape=(self.latent_dim,))#输入
        model.add(Dense(256*7*7, activation='relu',input_dim=self.latent_dim))#全连接
        model.add(BatchNormalization(momentum=0.8))
        model.add(Reshape((7,7,256)))
        #第一层转置卷积层
        model.add(Conv2DTranspose(128,kernel_size=3,strides=2,padding="same"))
        model.add(BatchNormalization(momentum=0.8))
        model.add(Activation('relu'))#激活函数
        #第二层转置卷积层
        model.add(Conv2DTranspose(64,kernel_size=3,strides=2,padding="same"))
        model.add(BatchNormalization(momentum=0.8))
        model.add(Activation('relu'))#激活函数
        #第三层转置卷积层
        model.add(Conv2DTranspose(32,kernel_size=3,padding="same"))
        model.add(BatchNormalization(momentum=0.8))
        model.add(Activation('relu'))#激活函数
        #第四层转置卷积层
        model.add(Conv2DTranspose(1,kernel_size=3,padding="same"))
        model.add(Activation('tanh'))#激活函数

        model.summary()#输出模型参数状况
        img = model(noise)
        plot_model(model, to_file='', show_shapes=True, show_layer_names=False, rankdir='TB')#绘制模型
        return Model(noise, img)

    #定义判别器模型
    #输入:28*28图像;输出:0-1数字
    def build_discriminator(self):
        model=Sequential()
        img=Input(shape=self.img_shape)
        #卷积层1
        model.add(Conv2D(64,kernel_size=3,strides=2,input_shape=self.img_shape,padding="same"))
        model.add(LeakyReLU(alpha=0.2))#激活函数
        model.add(Dropout(0.25))
        #卷积层2
        model.add(Conv2D(128,kernel_size=3,strides=2,padding="same"))
        model.add(LeakyReLU(alpha=0.2))#激活函数
        model.add(Dropout(0.25))
        #卷积层3
        model.add(Conv2D(256,kernel_size=3,strides=2,padding="same"))
        model.add(LeakyReLU(alpha=0.2))#激活函数
        model.add(Dropout(0.25))
        #卷积层4
        model.add(Conv2D(512,kernel_size=3,strides=1,padding="same"))
        model.add(LeakyReLU(alpha=0.2))#激活函数
        model.add(Dropout(0.25))
        #输出层
        model.add(Flatten())#铺平
        model.add(Dense(1,activation='sigmoid'))

        model.summary()#输出模型参数状况

        validity = model(img)
        plot_model(model, to_file='', show_shapes=True, show_layer_names=False, rankdir='TB')#绘制模型
        return Model(img,validity)

    #定义训练函数
    def train(self,epochs,batch_size=128,sample_interval=50):
        #创建训练数据集
        (X_train, _), (_, _) = mnist.load_data() # 从mnist数据集中获得数据
        X_train =X_train/127.5-1 # 标准化为(-1,1)
        X_train = np.expand_dims(X_train, axis=3) #60000*28*28变为60000*28*28*1;X_train的shape是60000*28*28*1
        
        #创建标签(0或者1)--二分类问题
        valid = np.ones((batch_size, 1))#全1阵--真
        fake = np.zeros((batch_size, 1))#全0阵--假

        for epoch in range(epochs):
            #训练discriminator
            idx=np.random.randint(0,X_train.shape[0],batch_size)#随机选择batch_size个数
            imgs=X_train[idx]#真图像

            noise = np.random.normal(0, 1, (batch_size, self.latent_dim))#生成正态分布噪声
            gen_imgs = self.generator.predict(noise)#假图像

            #训练discriminator
            d_loss_real = self.discriminator.train_on_batch(imgs, valid)#真图像训练
            d_loss_fake = self.discriminator.train_on_batch(gen_imgs, fake)#假图像训练
            d_loss = 0.5 * np.add(d_loss_real, d_loss_fake)#数据求和

            #训练生成器
            g_loss=self.combined.train_on_batch(noise,valid)
            print("steps:%d [D loss: %f, acc.: %.2f%%] [G loss: %f]"% (epoch, d_loss[0], 100*d_loss[1],g_loss))

            #迭代50次打印一次
            if epoch % sample_interval == 0:
                self.sample_images(epoch)


    def sample_images(self,epoch):
        r,c=5,5
        noise=np.random.normal(0,1,(r*c,self.latent_dim))#生成随机噪声
        gen_imgs = self.generator.predict(noise)#生成器预测的图像
        gen_imgs = 0.5 * gen_imgs + 0.5 #将generator输出的(-1,1)像素值反归一化为(0,1)
        #绘制5*5图像
        fig, axs = plt.subplots(r, c)
        cnt = 0
        for i in range(r):
            for j in range(c):
                axs[i,j].imshow(gen_imgs[cnt, :,:,0], cmap='gray')
                axs[i,j].axis('off')
                cnt += 1
        fig.savefig("images/%" % epoch)
        plt.close()


if __name__=='__main__':
    if not os.path.exists("./images"):
        os.makedirs("./images")
    dcgan = DCGAN()
    dcgan.train(epochs=5000, batch_size=256, sample_interval=200)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149

五、常见问题汇总

  1. 关于转置卷积、分数步长卷积与反卷积之间的联系与区别可以参考如下文章:/paper/A-guide-to-convolution-arithmetic-for-deep-learning-Dumoulin-Visin/f19284f6ab802c8a1fcde076fcb3fba195a71723?p2df

  2. 训练中需要注意的一些细节:
    ①对于用于训练的图像数据样本,仅将数据缩放到[-1,1]的范围内,这个也是tanh的取值范围,并不做任何其他处理。
    ②模型均采用mini-batch大小为128的批量随机梯度下降方法进行训练。权重的初始化使用满足均值为0、方差为0.02的高斯分布的随机变量。
    ③对于激活函数LeakyReLU,其中的Leak的部分设置斜率为0.2。
    ④训练过程中使用Adam优化器进行超参数调优,学习率使用0.0002,动量β取0.5,使得训练更加稳定。

  3. 在DCGAN训练过程中我遇到了如下情况:训练准确率达到100%,但是生成的图像却是乱七八糟的噪声图像,如下图:
    在这里插入图片描述
    在这里插入图片描述
    经过仔细研究,我感觉可能是生成器采用了上采样层(UpSampling2D层)导致的,因此我将上采样和卷积层全部替换为转置卷积后便没有出现这种现象。有知道的大佬欢迎在评论中指出~

六、【拓展】利用DCGAN实现三通道彩色图像生成

在上面的代码中,我们设计实现了单通道灰度图像——MNIST手写数字的生成,下面我们来实现利用DCGAN实现三通道彩色图像的生成。
由于上面已经详细介绍了DCGAN的基本原理和实现流程,因此话不多说,直接上结果:
在这里插入图片描述
相关代码对比生成灰度图像变化不大,有需要的可以参考:
/download/didi_ya/16201242

如果对你有所帮助,记得点个赞哟~