我们在第3章实现了一个GAN,其生成器和判别器是具有单个隐藏层的简单前馈神经网络。尽管很简单,但GAN的生成器充分训练后得到的手写数字图像的真实性有些还是很具说服力的。即使是那些无法被识别为人类手写数字的字符,也具有许多手写符号的特征,例如可辨认的线条边缘和形状,特别是与用作生成器原始输入的随机噪声相比,更是如此。
想象一下,如果使用更强大的网络架构可以实现什么?本章中的生成器和判别器都将使用卷积神经网络(CNN,或 ConvNet),而不再是简单的双层前馈网络。这种GAN架构称为深度卷积生成对抗网络( Deep Convolutional GAN, DCGAN)。
在深入探讨 DCGAN实现的细节之前,我们先在本章介绍 ConvNet的关键概念,回顾开发DCGAN背后的历史,并介绍使DCGAN这样复杂的架构在实践中变为可行的关键性突破之一:批归一化( batch normalization)。
一、卷积神经网络 1. 卷积滤波器常规前馈神经网络的神经元排列在平面的全连接层中,而 ConvNet中的层排列在三维(宽高深)中。卷积是通过在输入层上滑动一个或多个滤波器(filter)来执行的,每个滤波器都有一个相对较小的感受野(宽×高),但它贯穿输入图像的全部深度。
每个滤波器在输入图像上滑动每一步,都会输出一个激活值:它是输入值和过滤器值之间的点积,此过程将为每个滤波器生成一个二维的激活图( activation map)。将每个滤波器生成的激活图堆叠在一起可以形成一个三维输出层,其输出深度等于所用滤波器的数量。
2. 参数共享重要的是,给定滤波器参数被其所有输入值共享,这具有直观和实用的优点。直观地讲参数共享能够有效地学习视觉特征和形状(如线条和边缘),无论它们在输入图像中位于何处。从实际的角度来看,参数共享可以大大减少可训练参数的数量。这降低了过拟合的风险,并允许该技术在不增加可训练参数的情况下扩展到更高分辨率的图像中,而在同样情况下,传统全连接网络却要指数爆炸一样增加可训练参数才可以做到。
3. 卷积神经网络可视化如果这样解释听起来有些令人困感,那么通过可视化这些概念可以使它们不那么抽象。下图展示了单个卷积操作。描述了二维输入上单个滤波器的卷积运算。实际上,输入图像通常是三维的而且几个滤波器堆叠在一起使用。
一个3X3的卷积滤波器在一个5X5的输入上滑动——从左到右,从上到下。过滤器滑动步长为2,因此一共滑动4次,得到一个2X2的激活图。每次滑动,整个过滤器会产生一个激活值。
但基本机制是不变的:不管输入体积的深度如何,每个滤波器每一步产生一个值,使用的滤波器数量决定了输出图像的深度,因为它们生成的激活图相互叠加,如下图所示。
二、DCGAN简史filter和kernel之间的不同很微妙。很多时候,它们可以互换,所以这可能造成我们的混淆。那它们之间的不同在于哪里呢?一个"kernel"更倾向于是2D的权重矩阵。而"filter"则是指多个Kernel堆叠的3D结构。如果是一个2D的filter,那么两者就是一样的。但是一个3Dfilter, 在大多数深度学习的卷积中,它是包含kernel的。每个卷积核都是独一无二的,主要在于强调输入通道的不同方面。
DCGAN于2016年提出,自问世以来便成了GAN领域最重要的早期创新之一。这并不是研究人员第一次在GAN中使用 ConvNet的尝试,却是第一次成功的将ConvNet直接整合到完整的GAN模型中。
ConvNet的使用加剧了困扰GAN训练的许多困难,包括训练不稳定和梯度饱和。的确这些挑战是如此艰巨,以至于有些研究人员求助于其他方法,如拉普拉斯生成对抗网络( LAPGAN)——它使用拉普拉斯金字塔式的级联卷积网络,也就是说,在每一层使用GAN框架对单独的卷积神经网络进行训练。由于被更优越的方法所取代, LAPGAN在很大程度上已经被扔进了历史的垃圾筒,因此了解它的内部原理并不重要。
尽管 LAPGAN笨拙复杂且计算烦琐,但在其发布时仍提供了当时质量最高的图像,与原始GAN相比改进了4倍( LAPGAN有40%,原始GAN有10%的生成图像被人工评估者误认为是真实的)。因此, LAPGAN展示了将GAN与 ConvNet结合的巨大潜力。
在 DCGAN中, Radford和他的合作者引入了一些技术和优化方法,使 ConvNet可以扩展到完整的GAN框架,而无须修改底层的GAN架构,也不需要将GAN简化为更复杂的模型框架的子结构(如 LAPGAN)。Radford等人引入的关键技术之一就是使用了批归一化——通过归一化应用它的每一层的输入来帮助稳定训练过程。下面仔细看看什么是批归一化以及它又是怎么起作用的。
三、批归一化就像对网络输入进行归一化一样,在每个小批量训练数据通过网络时,对每个层的输入进行归一化。
1.理解归一化本节的内容有助于提醒我们什么是归一化以及为什么先要对输入特征值进行归一化。归一化( normalization)是数据的缩放,使它具有零均值和单位方差。这是通过取每个数据点x减去平均值\({\mu}\),然后除以标准偏差得到的,如下式所示。
\[\hat{x} = \frac{x - \mu}{\sigma} \]归一化有几个优点。最重要的一点或许是使得具有巨大尺度差异的特征之间的比较变得更容易,进而使训练过程对特征的尺度不那么敏感。下面考虑一个(虚构的)例子,假设我们尝试基于两个特征来预测一个家庭的每月支出:家庭的年收入和家庭成员数。一般而言,一个家庭的收入越多,家庭成员越多,支出就越多。
但是这两个特征的尺度截然不同:年收入增加10美元可能不会影响一个家庭的支出,但增加10个成员可能会严重影响任何一个家庭的预算。归一化通过将每个特征值缩放到一个标准化的尺度上解决了这个问题,这样一来每个数据点都不表示为其实际值,而是以一个相对的“分数”表示给定数据点与平均值的标准偏差。
批归一化背后所体现的理念是,在处理具有多层的深度神经网络时,仅规范化输入可能还远远不够。当输入值经过一层又一层网络时,它们将被每一层中的可训练参数进行缩放。当参数通过反向传播得到调整时,每一层输入的分布在随后的训练迭代中都容易发生变化,从而影响学习过程的稳定性。在学术界,这个问题称为协变量偏移( covariate shift)。批归一化通过按每个小批量的均值和方差缩放每个小批量中的值来解决该问题。
2. 计算批归一化批归一化的计算方式与之前介绍的简单归一化方程在几个方面有所不同。我们将在本节进行介绍。
令\(\mu_{B}\)为小批量B的平均值,\(\sigma^2_{B}\)为小批量B的方差(均方误差)。归一化值的计算如下式所示。
\[\hat{x} = \frac{x - \mu_{B}}{\sqrt{\sigma^2_{B}+\varepsilon}} \]增加\(\varepsilon\)项是为了保持数值稳定性,主要是为了避免被零除,一般设置为一较小的正常数,例如0.001。
在批归一化中不直接使用这些归一化值,而是将它们乘以\(\gamma\)并加上\(\beta\)后,再作为输入传递到下一层,如下式所示。
\[y = \gamma\hat{x}+\beta \]重要的是,\(\gamma\)和\(\beta\)是可训练的参数,就像权重和偏置一样在网络训练期间进行调整。这样做有助于将中间的输入值标准化,使其均值在0附近(但非0)。方差也不是1。\(\gamma\)和\(\beta\)是可训练的,因此网络可以学习哪些值最有效。
幸运的是,我们不必操心这些。Keras中的函数 Keras.layers.BatchNormalization可以处理所有小批量计算并在后台进行更新。
批归一化限制了更新前一层中的参数对当前层接收的输入分布可能的影响。这减少了跨层参数之间不必要的相互依赖,从而有助于加快网络训练并增强鲁棒性,特别是在网络参数初始化方面。
批归一化已被证明对包括 DCGAN在内的许多深度学习架构是否可行至关重要。
四、用DCGAN生成手写数字我们将在本节回顾第3章中的生成 MNIST手写数字。这次将使用 DCGAN架构,并将生成器和判别器都换成卷积网络,如下图所示。除此更改外,其余网络结构保持不变。在本教程的最后,我们将比较两个GAN(传统GAN与 DCGAN)生成的手写数字的质量,以展示更高级的网络结构带来的改进。
1. 导入模块并指定模型输入维度首先导入训练和运行模型所需的所有包、模块以及库。直接从keras.datasets导入MNIST手写数字数据集。
#导入声明
import matplotlib.pyplot as plt
import numpy as np
from keras.datasets import mnist
from keras.layers import (
Activation, BatchNormalization, Dense, Dropout, Flatten, Reshape
)
from keras.layers.advanced_activations import LeakyReLU
from keras.layers.convolutional import Conv2D, Conv2DTranspose#卷积层
from keras.models import Sequential
from keras.optimizers import Adam
#模型输入维度
img_rows = 28
img_cols = 28
channels = 1
img_shape = (img_rows, img_cols, channels)#输入图像的维度
z_dim = 100#用于输入生成器的噪声向量的大小
2. 构造生成器
ConvNet传统上用于图像分类任务,图像以尺寸——高度x宽度x彩色通道数作为输入,并通过一系列卷积层输出一个维数为1×n的类别得分向量,n是类别标签数。要使用ConvNet结构生成图像,则是上述过程的逆过程:并非获取图像再将其处理为向量,而是获取向量并调整其大小以使之变为图像。
这一过程的关键是转置卷积(transposed convolution)。我们通常使用卷积减小输入的宽度和高度,同时增加其深度。转置卷积与其相反,用于增加宽度和高度,同时减小深度,如下图的生成器网络图所示。(生成器将随机噪声向量作为输入并生成28×28×1的图像。这一过程通过多层转置卷积实现,在卷积层之间应用批归一化来稳定训练过程(图像未按比例绘制))
生成器从噪声向量z开始,使用一个全连接层将向量重塑为具有小的宽x高和大的深度的三维隐藏层。使用转置卷积对输入进行逐步重塑,以使其宽×高增大而深度减小,直到具有想要合成的图像大小28×28×1。
在每个转置卷积层之后,应用批归一化和 LeakyReLU激活函数;在最后一层不应用批归一化,并且使用tanh激活函数代替ReLU。
综合所有步骤如下。
- 取一个随机噪声向量z,通过全连接层将其重塑为7×7×256张量。
- 使用转置卷积,将7×7×256张量转换为14×14×128张量。
- 应用批归一化和 LeakyReLU激活函数。
- 使用转置卷积,将14×14×128张量转换为14×14×64张量。注意:宽度和高度尺寸保持不变。可以通过将Conv2DTranspose中的 stride参数设置为1来实现。
- 应用批归一化和 LeakyReLU激活函数。
- 使用转置卷积,将14×14×64张量转換为输出图像大小28×28×1。
- 应用tanh激活函数。
def build_generator(z_dim):
model = Sequential()
model.add(Dense(128*7*7, input_dim=z_dim))
model.add(LeakyReLU())
model.add(BatchNormalization())
model.add(LeakyReLU())
model.add(Reshape((7, 7, 128)))
model.add(Conv2DTranspose(64, 3, 2, padding='same'))
#步长为1时,padding为"SAME"可以保持输出与输入的尺寸具有相同的大小
#输出矩阵的大小:padding为SAME时:ceil(i/s),其中i表示输入矩阵的大小,s表示卷积核的步长,
#ceil函数表示向上取整。
#padding为VALID时:ceil((i-k+1)/s),k表示卷积核的尺寸。
model.add(LeakyReLU())
model.add(Conv2DTranspose(1, 3, 2, padding='same'))
model.add(Activation('tanh'))#带tanh激活函数的输出层
return model
3. 构造判别器
判别器是一种ConvNet,它接收图像并输出预测向量:在这种情况下,它是一种二值分类,表明输入的图像是否被认为是真实的而不是假的。如下图所示是我们将要实现的判别器网络。(判别器将28×28×1图像作为输入经过多个卷积层,使用sigmoid激活函数\(\sigma\)输入/输出图像是真实的概率。在卷积层之间应用批归一化来稳定训练过程(图像未按比例绘制))
判别器的输入是28×28×1的图像。应用卷积可以对图像进行变换,使其宽×高逐渐变小,深度逐渐变深。在所有卷积层中使用 LeakyReLU激活函数;批归一化用于除第一层以外的所有卷积层;输出使用全连接层和sigmoid激活函数。
综合所有步骤如下。
- 使用卷积层将28×28×1的输入图像转换为28×28×64的张量。
- 应用 LeakyReLU激活函数。
- 使用卷积层将28×28×64的张量转换为12×12×128的张量。
- 应用LeakyReLU激活函数。
- 使用卷积层将12×12×128的张量转换为4×4×128的张量。
- 应用LeakyReLU激活函数。
- 将4×4×128张量展成大小为4×4×128=2048的向量。
- 使用全连接层,输入sigmoid激活函数计算输入图像是否真实的概率。
def build_discriminator(img_shape):
model = Sequential()
model.add(Conv2D(64, 5, 1, padding='same', input_shape=img_shape))
model.add(LeakyReLU())
model.add(Conv2D(128, 5, 2))
model.add(LeakyReLU())
model.add(Conv2D(128, 5, 2))
model.add(Flatten())
model.add(Dense(1024))#随着网络深度增加,网络宽度也相应增加
model.add(LeakyReLU())
model.add(Dense(1))
model.add(Activation('sigmoid'))
#use_bias:一般来说要设成True。但是当卷积层后根由BatchNorm或者InstanceNorm层时,最好设为False,
#因为归一化层会归一化卷积层输出并且加上自己的bias,卷积层的(如果有)bias就是多余的了
return model
4. 构建并运行DCGAN
1. 完整代码
除了生成器和判别器的网络结构,DCGAN的其他设置和实现与第3章中简单GAN的网络相同。这体现了GAN架构的通用性。
def build_dcgan(generator, discriminator):
model = Sequential()
#生成器和判别器结合为一个模型
model.add(generator)
model.add(discriminator)
return model
#构建并编译判别器
discriminator = build_discriminator(img_shape)
discriminator.compile(
loss='binary_crossentropy',
optimizer=Adam(lr=1e-3),
metrics=['acc']
)
#构建生成器
generator = build_generator(z_dim)
#生成器训练时保持判别器参数不变
discriminator.trainable = False
#构建并编译判别器固定的GAN模型来训练生成器
dcgan = build_dcgan(generator, discriminator)
dcgan.compile(
loss='binary_crossentropy',
optimizer=Adam(lr=1e-3)
)
losses = []
accuracies = []
iteration_checkpoints = []
def train(iterations, batch_size, sample_interval):
(x_train, _), (_, _) = mnist.load_data()#加载mnist数据集
x_train = x_train / 127.5 - 1.0#灰度像素值从[0, 255]缩放到[-1, 1]
x_train = np.expand_dims(x_train, axis=3)
real = np.ones((batch_size, 1))#真实图像标签为1
fake = np.zeros((batch_size, 1))#伪图像标签为0
for iteration in range(iterations):
#获取一批真实图像
idx = np.random.randint(0, x_train.shape[0], batch_size)
imgs = x_train[idx]
#生成一批伪图像
z = np.random.normal(0, 1, (batch_size, 100))
gen_imgs = generator.predict(z)#生成batch_size个形状为(28, 28, 1)的图像
#训练判别器
d_loss_real = discriminator.train_on_batch(imgs, real)
d_loss_fake = discriminator.train_on_batch(gen_imgs, fake)
d_loss, accuracy = 0.5 * np.add(d_loss_real, d_loss_fake)#计算总的loos和acc的值:取真实图像和伪图像的均值
z = np.random.normal(0, 1, (batch_size, 100))#生成128行100列伪数据, 即一个数据有100列特征,生成128个数据(一个batchsize)
gen_imgs = generator.predict(z)
g_loss = gan.train_on_batch(z, real)
if (iteration + 1) % sample_interval == 0:
#保存训练损失和准确率以便训练后绘图
losses.append((d_loss, g_loss))
accuracies.appennd(100.0 * accuracy)
iteration_checkpoints.append(iteration + 1)
print("%d [D loss: :%f, acc.: %.2f] [G loss: %f]" % (iteration + 1, d_loss, 100.0 * accuracy, g_loss))
sample_image(generator)#输出生成图像的采样
#显示生成图像
def sample_image(generator, image_grid_rows=4, image_grid_columns=4):
z = np.random.normal(0, 1, (image_grid_rows * image_grid_columns, z_dim))#随机噪声采样
gen_imgs = generator.predict(z)#从随机噪声生成图像
gen_imgs = 0.5 * gen_imgs + 0.5#图像缩放到[0, 1]:[-1, 1]--->[0, 1]
fig, axs = plt.subplots(
img_rows,
img_cols,
figsize(16, 16),
sharex=True,
sharey=True
)
cnt = 0
for i in range(image_grid_rows):
for j in range(image_grid_columns):
axs[i, j].imshow(gen_imgs[cnt, :, :, 0], cmap='gray')
axs[i, j].axis('off')
cnt += 1
#运行模型
iterations = 20000
batch_size = 128
sample_interval = 1000
train(iterations, batch_size, sample_interval)
2. 模型输出
充分训练后的DCGAN的生成器生成的手写数字如下图左所示,为了便于同时比较,GAN生成的数字样本如下图右所示。
下图是MNIST真实手写数字样本。可以看到DCGAN生成的手写数字与真实样本几乎没有区别。
五、结论DCGAN展示了GAN框架的通用性。理论上来说,判别器和生成器可以用任何可微函数表示,甚至可以用多层卷积网络这样复杂的函数表示。但是 DCGAN也表明,要使更复杂的实现在实践中真正起作用,还存在很大的障碍。没有批归一化等突破性技术, DCGAN将无法正确训练。
六、小结- 卷积神经网络( ConvNet)使用一个或多个在输入图像上滑动的卷积滤波器。在输入图像上滑动的每一步,滤波器都会使用一组参数来产生一个激活值。来自所有滤波器的所有激活值共同生成输出层。
- 批归一化是指在将每一层的输出作为输入传递到下一层之前,对其进行归一化,以减小神经网络中协变量偏移(训练期间各层之间输入值分布的变化)。
- 深度卷积生成对抗网络( DCGAN)以卷积神经网络为生成器和判别器。它在图像处理任务(如手写数字生成)中实现了优异的性能。