生成式对抗网络(GAN)
生成式对抗网络
Ian Goodfellow等人在2014年的论文中提出了生成式对抗网络,尽管这个想法立刻使研究人员们兴奋不已,但还是花了几年时间才克服了训练GAN的一些困难。就像许多伟大的想法一样,事后看起来似乎很简单:让神经网络竞争,希望这种竞争能够促使它们变得更好。GAN由两个神经网络组成
- 生成器
以随机分布作为输入(通常是高斯分布),并输出一些数据(通常是图像)。可以将随机输入视为要生成的图像的潜在表征(即编码)。因此可以看到,生成器提供的功能与变分自动编码器中的解码器相同,并且可以使用相同的方式来生成新图片(是需要馈入一些高斯噪声,就会输出一个新图片) - 判别器
输入从生成器得到的伪图像或从训练集中得到的真实图像,并且必须猜测输入图像是伪图像还是真实图像
在训练过程中,生成器和判别器有相反的目标:判别器试图从真实图像中分别出虚假图像,而生成器则试图产生看起来足够真实的图像来欺骗判别器。由于GAN由不同目标的两个网络组成,因此无法像常规神经网络一样对其进行训练。每个训练迭代都分为两个阶段
- 在第一阶段,训练判别器。从训练集中采样一批真实图像,再加上用生成器生成的相等数量的伪图像组成训练批次。对于伪图像,将标签设置为0;对于真实图像将标签设置为1,并使用二元交叉熵损失在该被标签的批次上对判别器进行训练。重要的是,在这个阶段反向传播只能优化判别器的权重
- 在第二阶段,训练生成器。首先使用它来生成另一批伪图像,然后再次使用判别器来判断图像是伪图像还是真实图像。在这个批次中不添加真实图像,并且将所有标签都设置为1(真实):换句话说,希望生成器能产生判别器(骗过判别器,让判别器认为是真的)会认为是真实的图像。至关重要的是,在此步骤中,判别器的权重会被固定,因此反向传播只会影响生成器的权重
生成器实际上从未看到过任何真实的图像,但是它会逐渐学会令人信服的伪图像。它所得到的是流经判别器的回流梯度。幸运的是,判别器越好,这些二手梯度中包含的真实图像信息越多,因此生成器可以取得很大的进步
现在给Fashion MNIST构建一个简单的GAN
首先,需要构建生成器和判别器。生成器类似于自动编码器的解码器,判别器是常规的二元分类器(它以图像作为输入,包含单个神经元和使用sigmoid激活函数的Dense层)。对于每个训练迭代的第二阶段,还需要一个完整的GAN模型,其中包含生成器,后面跟随一个判别器:
import tensorflow as tf
from tensorflow import keras
codings_size = 30
generator = keras.models.Sequential([
keras.layers.Dense(100, activation='gelu', input_shape=[codings_size]),
keras.layers.Dense(150, activation='gelu'),
keras.layers.Dense(28 * 28, activation='sigmoid'),
keras.layers.Reshape([28, 28])
])
discriminator = keras.models.Sequential([
keras.layers.Flatten(input_shape=[28, 28]),
keras.layers.Dense(150, activation='gelu'),
keras.layers.Dense(100, activation='gelu'),
keras.layers.Dense(1, activation='sigmoid')
])
gan = keras.models.Sequential([generator, discriminator])
接下来,需要编译模型。由于判别器是二元分类器,可以使用二元交叉熵损失。生成器仅通过gan模型进行训练,因此不需要对其进行编译。gan模型也是二元分类器,因此可以使用二元交叉熵损失。重要的是,判别器不应该在第二阶段进行训练,因此在训练gan模型之前,将其设为不可训练:
discriminator.compile(loss='binary_crossentropy', optimizer='rmsprop')
discriminator.trainable = False
gan.compile(loss='binary_crossentropy', optimizer='rmsprop')
Keras仅在编译模型时才考虑可训练属性,因此在运行此代码后,如果调用其fit()方法或其train_on_batch()方法,则判别器是可以训练的;当在gan模型上调用这些方法时,则判别器是不可训练的
由于训练循环不寻常,因此不能使用常规的fit()方法,相反,需要写一个自定义训练循环。为此,首先需要创建一个数据集来遍历数据集
fashion_mnist = keras.datasets.fashion_mnist
(X_train_all, y_train_all), (X_test, y_test) = fashion_mnist.load_data()
X_train_all, y_train_all, X_test, y_test = tf.cast(X_train_all, tf.float32), tf.cast(y_train_all, tf.float32), tf.cast(
X_test, tf.float32), tf.cast(y_test, tf.float32)
batch_size = 32
dataset = tf.data.Dataset.from_tensor_slices(X_train_all / 255.).shuffle(1000)
dataset = dataset.batch(batch_size, drop_remainder=True).prefetch(1)
现在编写训练循环,将其包装在train_gan()函数中
def train_gan(gan, dataset, batch_size, codings_size, n_epochs=50):
generator, discriminator = gan.layers
for epoch in range(n_epochs):
for X_batch in dataset:
noise = tf.random.normal(shape=[batch_size, codings_size])
generated_images = generator(noise)
X_fake_and_real = tf.concat([generated_images, X_batch], axis=0)
y1 = tf.constant([[0.]] * batch_size + [[1.]] * batch_size)
discriminator.trainable = True
discriminator.train_on_batch(X_fake_and_real, y1)
noise = tf.random.normal(shape=[batch_size, codings_size])
y2 = tf.constant([[1.]] * batch_size)
discriminator.trainable = False
gan.train_on_batch(noise, y2)
train_gan(gan, dataset, batch_size, codings_size)
coding = tf.random.normal(shape=[12, codings_size])
images = generator(coding).numpy()
import matplotlib.pyplot as plt
def plot_image(image):
plt.imshow(image, cmap='binary')
plt.axis('off')
fig = plt.figure(figsize=(12 * 1.5, 3))
for image_index in range(12):
plt.subplot(3, 4, image_index + 1)
plot_image(images[image_index])
- 在第一阶段,将高斯噪声馈送到生成器以生成伪图像,然后通过合并相等数量的真实图像来组成这一批次,对于伪图像,目标y1被设置为0;对于真实图像,目标y1被设置为1。然后在这个批次上进行判别其训练。这里将判别器的1可训练属性设置为True:这只是为了消除Keras注意到在编译模型时可训练属性为True,但是现在为False时显示的警告
- 在第二阶段,向GAN馈入一些高斯噪声。它的生成器首先会生成伪图像,然后判别器会试图猜测这些图像是伪图像还是真实图像。我们希望判别器相信伪图像是真实的,因此将目标y2设置为1。再次将可训练属性设置为False,以避免再次发出警告
GAN的训练难点
在训练过程中,生成器和判别器在零和游戏中不断地试图超越彼此。随着训练的进行,游戏可能会在一种博弈论者称为纳什均衡的状态中结束,这种均衡以数学家约翰·纳什的名字命名:在这种情况下,假设其他玩家都没有改变他们的策略,那么没有人会改变自己的策略使自己变得更好。例如,当每个人都在道路左侧行驶时,达到了纳什均衡:当么给人都在道路的右侧行驶时。不同的初始状态和动态发展可能会导致一个平衡或另一个平衡。在此示例中,一旦达到平衡(即与其他所有人在同一侧行驶),就存在一个最佳策略,但是一个纳什均衡可能涉及多种竞争策略(例如,捕食者追捕猎物,猎物试图逃避,改变它们的策略也不会变得更好)
那么如果适用于GAN,论文的作者证明了GAN只能达到单个纳什均衡:当生成器产生完美逼真的图像时,判别器只能被迫猜测(50%真实,50%伪造)。这个事实非常令人鼓舞:似乎只需要训练GAN足够长的时间,它最终就会达到均衡,从而给出一个完美的生成器。不幸的是,并不是那么简单:没有任何东西可以保证达到均衡
最大的困难被称为模式崩溃:就是当生成器的输入逐渐变得不太多样化时。这是怎么发生的?假设生成器在产生逼真的鞋子方面比其他任何类都更好。他就会用鞋子来更多地欺骗判别器,这会鼓励它生成更多地鞋子图像。逐渐地它会忘记如何产生其他任何东西。同时判别器看到的唯一伪造图像将是鞋子,因此它也会忘记如何辨别其他类别的伪造图像。最终当判别器在进行假鞋和真鞋区分时,生成器将被迫转移到另一类。这样以来,它可能会变得擅长于生成衬衫,而忘记了鞋子,然后判别器也会跟着生成器。GAN可能会逐渐在几个类别中循环,而不会擅长于生成任何一个类别
此外由于生成器和判别器不断地相互竞争,因此它们的参数可能最终会振荡并开始变得不稳定。训练开始时可能会很好,然后由于这些不稳定而突然发散,没有明显的原因。而且有许多因素会影响这些复杂的动态过程,因此GAN对超参数非常敏感:可能不得不花费大量的精力来微调它们
自2014年以来,研究人员忙于解决这些问题:针对该问题发表了许多论文,其中一些提出了新的成本函数(尽管Google研究人员在2018年发表了一篇论文质疑其效率)或技术来解决训练稳定性或避免模式崩溃的问题。例如,一种称为重播体验的流行技术包括将生成器在每次迭代中生成的图像存储在重播缓存区中(逐渐删除较早生成的图像),使用真实图像以及从该缓冲区中取出的伪图像来训练判别器(而不是由当前生成器生成的伪图像)。这减少了判别器过拟合最新生成输出的图像的机会。另一种常见的技术称为小批量判别:它可测量跨批次中相似图像的程度,并将此统计信息提供给判别器,因此判别器可以轻松拒绝缺乏多样性的一整个批次的伪图像。这会鼓励生成器生成更多样的图像,从而减少模式崩溃。其他论文只是提出了一些表现良好的特定网络架构
深度卷积GAN
2014年的GAN原始论文试验了卷积层,但只是生成了小图像。不久之后,许多研究人员试图基于更深的卷积网络为更大的图像构建GAN。由于训练非常不稳定,所以被证明是棘手的,但是Alec Radford等人在实验了许多不同的架构和超参数后,终于在2015年末取得了成功。他们称其架构为深度卷积GAN(DCGAN)。以下是他们为构建稳定的卷积GAN提出的主要指导
- 用跨步卷积(在判别器中)和转置卷积(在生成器中)替换所有池化层
- 除生成器的输出层和判别器的输入层,在生成器和判别器中都使用批量归一化
- 删除全连接的隐藏层以获得更深的架构
- 对生成器中所有层使用ReLU激活函数,除了输出层应该使用tanh
- 对判别器中所有层使用leaky ReLU激活函数
这些准则在许多情况下都会起作用,但并非总是如此,因此可能需要试验不同的超参数(实际上,仅仅更改随机种子并再次训练相同的模型有时会起作用),下面是一个小型DCGAN,在Fashion MNIST数据集上可以很好地工作:
codings_size = 100
generator = keras.models.Sequential([
keras.layers.Dense(7 * 7 * 128, input_shape=[codings_size]),
keras.layers.Reshape([7, 7, 128]),
keras.layers.BatchNormalization(),
keras.layers.Conv2DTranspose(64, kernel_size=5, strides=2, padding='same', activation='relu'),
keras.layers.BatchNormalization(),
keras.layers.Conv2DTranspose(1, kernel_size=5, strides=2, padding='same', activation='tanh')
])
discriminator = keras.models.Sequential([
keras.layers.Conv2D(64, kernel_size=5, strides=2, padding='same', activation=keras.layers.LeakyReLU(.2),
input_shape=[28, 28, 1]),
keras.layers.Dropout(.4),
keras.layers.Conv2D(128, kernel_size=5, strides=2, padding='same', activation=keras.layers.LeakyReLU(.2)),
keras.layers.Dropout(.4),
keras.layers.Flatten(),
keras.layers.Dense(1, activation='sigmoid')
])
gan = keras.models.Sequential([generator, discriminator])
生成器使用大小为100的编码,将其投影到6272维度(77128),对结果进行重构以获得77128张量。该张量被批量归一化后,馈入步幅为2的转置卷积层层,将其从77上采样至1414,将深度从128缩小至64.其结果在此被批量归一化,并馈入另一个步幅为2的转置卷积层,将其从1414上采样到2828,将深度从64减少到1.该层使用tanh激活函数,因此输入范围为-1。因此在训练GAN之前,需要将训练集重新按比例调整为相同的范围。还需要重构形状来添加通道维度:
X_train_all = tf.reshape(X_train_all, [-1, 28, 28, 1]) * 2. - 1.
discriminator.compile(loss='binary_crossentropy', optimizer='rmsprop')
discriminator.trainable = False
gan.compile(loss='binary_crossentropy', optimizer='rmsprop')
dataset = tf.data.Dataset.from_tensor_slices(X_train_all).shuffle(1000)
dataset = dataset.batch(batch_size, drop_remainder=True).prefetch(1)
train_gan(gan, dataset, batch_size, codings_size)
coding=tf.random.normal(shape=[batch_size, codings_size])
images = generator(coding).numpy()
fig = plt.figure(figsize=(12, 8))
for image_index in range(batch_size):
plt.subplot(4, 8, image_index + 1)
plot_image(images[image_index])
GAN的逐步增长
Nvidia的研究人员Tero Karras等人在2018年的一篇论文中提出了一项重要技术:他们建议在训练开始时生成小图像,然后向生成器和判别器中逐渐添加卷积层以生成越来越大的图像。这种方法类似于堆叠式自动编码器的贪婪分层训练。额外的层添加在生成的末尾和判别器的开始处,并且先前训练过的层仍是可训练的
例如,当生成器的输出从44增长到88时,一个上采样层(使用最近邻滤波)被添加到现有的卷积层中,因此它输出88特征图,然后将其馈送到新的卷积层(使用‘same’填充和1的步幅,因此其输出也为88)。这个新层之后是新的输出卷积层:这是内核为1的常规卷积层,它将输出向下投影到所需数量的颜色通道。为了避免在添加新卷积层时破坏第一个卷积层的训练权重,最终输出是原始输出层和新输出层的加权和。新输出的权重为a,而原始输出的权重是1-a,并且a从0缓慢增加到1.换句话说,新的卷积层逐渐增强,而原始输出层逐渐减弱。在新的卷积层添加到判别器时,使用类似的增强/减弱技术
这篇论文还介绍了其他几种旨在增加输出多样性(避免模式崩溃)和使训练稳定的技术:
-
小批次标准化层
在判别器末尾附近添加。对于输入中的每个位置,它计算批次中所有通道和所有实例的标准差(S=tf.math.reduce_std(inputs,axis=[0,-1]))。然后,将这些标准差在所有点上取平均值,得到一个单一值(v=tf.reduce_mean(S))。最后,向批量处理中的每个实例中添加一个额外的特征图,并用计算值填充(tf.concat([inputs,tf.fill([batch_size,height,width,1],v)],axis=-1))。如果生成器生成的图像变化不大,那么判别器中特张图上的标准差就会小很多。多亏了这一层,判别器就可以轻松访问此统计信息,从而减少被生成器(生成很少的多样性)欺骗的可能性。这会鼓励生成器产生不同的输出,从而降低模式崩溃的风险。 -
均衡的学习率
使用均值为0和标准差为1的简单高斯分布而不是使用He初始化来初始化所有权重。但是,权重会在运行时(即每次执行层时)按与He初始化相同的系数来缩小:权重除以\(\sqrt{2/n_{inputs}}\),其中\(n_{inputs}\)是层输入的数量。论文表明,当使用RMSProp、Adam或其他自适应梯度优化器时,该技术显著提高了GAN的性能。实际上,这些优化器通过估计的标准差对梯度更新进行归一化,因此动态范围较大的参数需要花费较长的训练时间,而动态范围较小的参数可能更新得太快,从而导致不稳定。通过将权重调整作为模型本身的一部分,而不仅仅是在初始化时进行权重调整,这种方法可确保在整个训练过程中,所有参数的动态范围都相同,因此它们都以相同的速度学习。这加快和稳定了训练过程 -
像素归一化层
在生成器中的每个卷积层之后添加。它基于同一图像中和同一位置但所有通道之间的所有激活对每个激活进行归一化(除以均方激活的平方根),在TensorFlow代码中,这是inputs/tf.sqrt(tf.reduce_mean(tf.square(X),axis=-1,keepdims=True)+1e-8)(需要平滑项来避免被零除)。该技术避免了由于生成器和判别器之间过度竞争而导致的激活爆炸
StyleGAN
相同的Nvidia团队在2018年发表的一篇论文中提出了高分辨率图像生成的最新技术,该论文介绍了流行的StyleGAN架构。作者在生成器中使用了风格转换技术,以确保生成的图像在各个尺度上都具有与训练图像相同的局部结构,从而极大地提高了所生成的图像质量。判别器和损失函数没有被修改,仅仅修改了生成器
-
映射网络
一个8层的MLP把潜在表示\(z\)(即编码)映射到向量\(w\)。然后,通过多个仿射变换(没有激活的Dense层)发送此向量,从而生成多个向量。这些向量从细粒度的纹理(例如头发的颜色)到高级特征(例如成人或儿童)在不同级别上控制生成图像的风格。简而言之,映射网络映射到多个风格向量 -
合成网络
负责生成图像。它具有恒定的学习输入(需要明确的是,此输入在训练后将保持不变,但是在训练过程中,它会经过反向传播不断调整)。如前所述,它通过多个卷积和上采样层处理此输入,但是有两处调整:首先,在卷积层的输入和所有输出中添加了一些噪声(在激活函数之前)。其次,每个噪声层后面是一个自适应实例归一化层(AdaIN):它独立地归一化每个特征图(通过减去特征图的均值并处以其标准差),然后使用风格向量确定每个向量图的比例和偏移量(风格向量为每个特征图包含一个比例和一个偏置项)
独立于编码来增加噪声的想法非常重要。图像的某些部分非常随机,例如每个雀斑或头发的确切位置。在较早的GAN中,这种随机性要么来自编码,要么是生成器自身产生的一些伪随机噪声。如果它来自编码,则意味着生成器使用了编码的表征力的很大一部分来存储噪声:这非常浪费。而且,噪声必须能够流经网络并达到生成器的最后一层:这似乎是不必要的约束,可能会减慢训练速度。最后,可能会出现一些人工视觉,因为在不同层次使用了相同的噪声。相反,如果生成器试图自己产生自己的伪随机噪声,该噪声可能看起来不那么令人信服,从而导致更多人工视觉。另外,生成器的权重的一部分用于产生伪随机噪声,这似乎又是浪费的。通过增加额外的噪声输入,可以避免所有这些问题。GAN能够使用所提供的噪声为图像的每个部分添加适当数量的随机性
每个级别增加的噪声都不相同。每个噪声输入由一个充满了高斯噪声的单个特征图组成,该噪声会广播到所有(给定级别的)的特征图,并在添加之前使用学习到的每个特征图的比例因子进行缩放
最后,StyleGAN使用一种称为混和正则化(或风格混合)的技术,使用两种不同的编码生成一定百分比的生成图像。具体来说,编码\(c_1\)和\(c_2\)通过映射网络发送,给出两个风格向量\(w_1\)和\(w_2\)。然后,合成网络基于第一个级别的风格\(w_1\)和其余级别的样式\(w_2\)生成图像。阶段级别是随机选择的。这可以防止网络假设相邻级别的风格是相关联的,这反过来又鼓励了GAN中的局部性,意味着每个风格向量仅影响所生成图像中有限数量的特征