AIGC实战——生成对抗网络
- 0. 前言
- 1. 生成对抗网络
- 1.1 生成对抗网络核心思想
- 1.2 深度卷积生成对抗网络
- 2. 数据集分析
- 3. 构建深度卷积生成对抗网络
- 3.1 判别器
- 3.2 生成器
- 3.3 DCGAN 模型训练
- 4. GAN 训练技巧
- 4.1 判别器强于生成器
- 4.2 生成器强于判别器
- 4.3 信息量不足
- 4.4 超参数
- 小结
- 系列链接
0. 前言
生成对抗网络 (Generative Adversarial Network
, GAN
) 是由 Ian Goodfellow
等人在 2014
年提出的一种强大的深度学习模型,可以用于生成新数据样本,比如图像、音频、文本等。GAN
包含两个神经网络:生成器和判别器。生成器根据输入的噪声信号生成一些伪造的数据样本,而判别器则负责判断该数据样本是真实的还是伪造的。在本节中,首先阐述生成对抗网络的理论基础,然后使用 Keras
构建生成对抗网络模型。
1. 生成对抗网络
1.1 生成对抗网络核心思想
生成对抗网络 (Generative Adversarial Network
,GAN
) 可以通过类比为一个伪造专家与一个检测专家之间的博弈。生成器 (generator
) 是伪造专家,目标是制造出逼真的假样本,以尽可能地欺骗检测专家。而鉴别器 (discriminator
) 则是检测专家,旨在区分真实样本和生成器产生的假样本,并尽可能准确地识别它们。
在开始时,生成器只能制造出不完美的假样本,而鉴别器擅长辨别真伪并且准确率相对较高。然而,随着训练的进行,生成器变得更加熟练,并努力提高自己的技艺,从而创造逼真的样本。同时,鉴别器也在不断学习和调整自己的判别能力,以保持对假样本的敏感度。
通过反复的博弈和训练,生成器和鉴别器逐渐达到一种平衡状态。生成器学会了创造足以迷惑鉴别器的逼真作品,而鉴别器也不断提高自己的判别能力。最终,生成器能够创造出几乎无法与真实样本区分的逼真假样本,而鉴别器也变得越来越难以区分真伪。
GAN
的核心思想是生成器和判别器之间的对抗,生成器试图将随机噪声转换为看起来像是从原始数据集中采样而来的观测样本,而判别器则试图预测一个观测样本是来自原始数据集还是生成器的伪造品。下图展示了生成器与判别器的输入和输出的示例。
在开始时,生成器会输出充满噪声的图片,同样判别器的预测也是随机的。GAN
的关键在于交替训练这两个网络,才能让生成器越来越擅长欺骗判别器,而判别器必须通过训练保证其正确识别真伪的能力,这又会驱使生成器找到欺骗判别器的新方法,从而推动循环不断向前进行。
1.2 深度卷积生成对抗网络
为了更深入了解生成对抗网络的训练流程,我们将使用 Keras
构建深度卷积生成对抗网络 (Deep Convolutional GAN
, DCGAN
),来生成积木的图片。GAN
的原论文中没有使用卷积层,而是使用了全连接层,但实践已经证明,卷积层能够提供更强大的性能,因此现在基本上所有的 GAN
架构都包含卷积层。
2. 数据集分析
首先,需要下载训练数据,使用 Kaggle
上的乐高积木图像数据集。该数据集中包含了 40,000
张不同角度的 50
种不同玩具积木的照片,示例样本图片如下所示:
可以通过 Kaggle 官方网站下载数据集,下载完成后将这些图像和相关的元数据保存到 ./data
文件夹中。
使用 Keras
的 image_dataset_from_directory
函数来创建一个指向存储图像目录的 TensorFlow
数据集,以便可以在需要时(例如,在训练期间)将图像批量读入内存,从而能够处理大型数据集,而无需担心将整个数据集都放入内存中导致内存不足的情况。同时将图像尺寸调整为 64 × 64
,并在像素值之间进行插值处理:
train_data = image_dataset_from_directory(
"data/lego_brick",
label_mode=None,
color_mode="grayscale",
image_size=(IMAGE_SIZE, IMAGE_SIZE),
batch_size=BATCH_SIZE,
shuffle=True,
interpolation="bilinear",
)
原始数据的像素强度范围为 [0, 255]
。在训练 GAN
时,我们将数据缩放到范围 [-1, 1]
,以便在生成器的最后一层使用 tanh
激活函数。相对于 sigmoid
函数,tanh
激活函数的梯度更大,有助于更快地收敛:
def preprocess(img):
"""
Normalize and reshape the images
"""
img = (tf.cast(img, "float32") - 127.5) / 127.5
return img
train = train_data.map(lambda x: preprocess(x)).as_numpy_iterator()
3. 构建深度卷积生成对抗网络
3.1 判别器
鉴别器的目标是预测图像是真实的还是伪造的,这属于图像分类问题,因此我们可以使用常见的深度神经网络架构,即先堆叠的卷积层,最后使用带有一个输出节点的全连接层。判别器的完整架构如下:
使用 Keras
实现判别器:
# 定义判别器的输入(图像)层
discriminator_input = layers.Input(shape=(IMAGE_SIZE, IMAGE_SIZE, CHANNELS))
# 堆叠 Conv2D 层,并在其间添加 BatchNormalization、LeakyReLU 激活和 Dropout 层
x = layers.Conv2D(64, kernel_size=4, strides=2, padding="same", use_bias=False)(
discriminator_input
)
x = layers.LeakyReLU(0.2)(x)
x = layers.Dropout(0.3)(x)
x = layers.Conv2D(
128, kernel_size=4, strides=2, padding="same", use_bias=False
)(x)
x = layers.BatchNormalization(momentum=0.9)(x)
x = layers.LeakyReLU(0.2)(x)
x = layers.Dropout(0.3)(x)
x = layers.Conv2D(
256, kernel_size=4, strides=2, padding="same", use_bias=False
)(x)
x = layers.BatchNormalization(momentum=0.9)(x)
x = layers.LeakyReLU(0.2)(x)
x = layers.Dropout(0.3)(x)
x = layers.Conv2D(
512, kernel_size=4, strides=2, padding="same", use_bias=False
)(x)
x = layers.BatchNormalization(momentum=0.9)(x)
x = layers.LeakyReLU(0.2)(x)
x = layers.Dropout(0.3)(x)
x = layers.Conv2D(
1,
kernel_size=4,
strides=1,
padding="valid",
use_bias=False,
activation="sigmoid",
)(x)
# 将最后一个卷积层展平,得到张量的形状为 1×1×1,因此不再需要 Dense 层
discriminator_output = layers.Flatten()(x)
# 定义判别器模型,该模型接受一个输入图像,并输出一个介于 0 和 1 之间的概率值
discriminator = models.Model(discriminator_input, discriminator_output)
print(discriminator.summary())
需要注意的是,为了缩小张量在网络中传递时的空间形状(从原始图像的 64
开始,然后是 32
、16
、8
、4
,最后是 1
),某些 Conv2D
层的步幅为 2
,同时增加通道数(灰度输入图像中通道数为 1
,然后变为 64
、128
、256
,最后为 512
),最后展平为单个预测值。
在最后一个 Conv2D
层上使用 sigmoid
激活函数,以确保输出值介于 0
和 1
之间,表示预测图像为真的概率。
3.2 生成器
接下来,构建生成器。生成器的输入是从多元标准正态分布中抽取的向量,输出是与原始训练数据中的图像大小相同的图像。GAN
的生成器和变分自编码器 (Variational Autoencoder
, VAE
) 的解码器具有完全相同的目标:将潜空间中的向量转换为图像。在生成模型中,从潜空间映射回原始域的概念非常常见,因为通过这个概念,我们能够在潜空间中操作向量以改变原始域中图像的高级特征。生成器架构如下所示:
使用 Keras
实现以上生成器模型:
# 定义生成器的输入层,长度为 100 的随机向量
generator_input = layers.Input(shape=(Z_DIM,))
# 使用 Reshape 层将输入向量转化为 1×1×100 的张量,以便应用转置卷积操作
x = layers.Reshape((1, 1, Z_DIM))(generator_input)
# 堆叠四个 Conv2DTranspose 层,并在其间添加 BatchNormalization 和 LeakyReLU 层
x = layers.Conv2DTranspose(
512, kernel_size=4, strides=1, padding="valid", use_bias=False
)(x)
x = layers.BatchNormalization(momentum=0.9)(x)
x = layers.LeakyReLU(0.2)(x)
x = layers.Conv2DTranspose(
256, kernel_size=4, strides=2, padding="same", use_bias=False
)(x)
x = layers.BatchNormalization(momentum=0.9)(x)
x = layers.LeakyReLU(0.2)(x)
x = layers.Conv2DTranspose(
128, kernel_size=4, strides=2, padding="same", use_bias=False
)(x)
x = layers.BatchNormalization(momentum=0.9)(x)
x = layers.LeakyReLU(0.2)(x)
x = layers.Conv2DTranspose(
64, kernel_size=4, strides=2, padding="same", use_bias=False
)(x)
x = layers.BatchNormalization(momentum=0.9)(x)
x = layers.LeakyReLU(0.2)(x)
# 最后一个 Conv2DTranspose 层使用 tanh 激活函数将输出转换到区间 [-1, 1],以匹配原始图像域
generator_output = layers.Conv2DTranspose(
CHANNELS,
kernel_size=4,
strides=2,
padding="same",
use_bias=False,
activation="tanh",
)(x)
# 定义生成器模型,其接受长度为100的向量并输出形状为 [64, 64, 1] 的张量
generator = models.Model(generator_input, generator_output)
print(generator.summary())
需要注意的是,为了增加张量在网络中传递时的空间形状(原始向量中为 1
,然后是 4
、8
、16
、32
,最后是 64
),某些 Conv2DTranspose
层的步幅为 2
,同时减少通道数(首先是 512
,然后是 256
、128
、64
,最后是 1
,以匹配灰度图像输出)。
UpSampling2D 与 Conv2DTranspose
除了使用 Conv2DTranspose
层来增加图像尺寸外,还可以使用 UpSampling2D
层,并在 UpSampling2D
后添加一个步幅为 1
的普通 Conv2D
层:
x = layers.UpSampling2D(size = 2)(x)
x = layers.Conv2D(256, kernel_size=4, strides=1, padding="same")(x)
UpSampling2D
层简单地将输入的每一行和每一列重复,用以将张量的宽度和高度加倍。然后,利用步幅为 1
的 Conv2D
层执行卷积操作。这种方法与转置卷积类似,但不同的是,上采样只是重复现有的像素值,而不是用零填充像素之间的空隙。
实践证明,Conv2DTranspose
方法会导致输出图像中出现伪影,如下图中所示的细小棋盘格形状,这会破坏输出的质量。然而,在许多性能优异的 GAN
中仍然使用转置卷积。
这两种方法,UpSampling2D + Conv2D
和 Conv2DTranspose
,都可以用于将随即向量转换回原始图像域,在实践中,我们可以尝试不同方法,以获得最佳生成效果。
3.3 DCGAN 模型训练
DCGAN
中生成器和判别器的架构非常简单,与 VAE
模型并没有太大的区别。理解 GAN
的关键在于了解生成器和判别器的训练过程。
为了训练判别器,我们可以创建一个训练集,其中一些图像是来自训练集的真实观测样本,还有一些图像是生成器的输出结果,真实图像对应的标签为 1
,生成图像对应点标签为 0
。如果将其视为一个监督学习问题,那么就可以训练判别器如何区分原始图像和生成图像之间的差异,输出结果表示输入图像为真实图像的概率,使用二元交叉熵作为损失函数。
在训练生成器时,我们需要找到一种评价每个生成的图像的方法,以便它可以优化图像。判别器正是这种评价方法,生成器可以生成一批图像,并将其通过判别器以获取每个图像的真实性概率得分。为了让生成的图像欺骗判别器,即我们希望判别器对生成器生成的图片输出结果接近于 1
,因此生成器的损失函数就是判别器的输出概率与一个全为1的向量之间的二元交叉熵。
因此,为了训练生成器,我们必须将生成器连接到判别器体,具体而言,我们将生成器的输出图像输入到判别器后,这个组合模型就可以根据判别器,输出生成图像为真的概率
为了训练这个组合模型,我们必须交替训练这两个网络,并确保一次只更新一个网络的权重。例如,在生成器训练过程中,我们必须冻结判别器的权重,只更新生成器的权重。如果我们也允许判别器的权重发生变化,那么判别器只会调整自己,以便更有可能将生成的图像预测为真实图像,而这不是我们期望的结果。我们希望生成的图像被预测接近 1
(真实)是由于生成器性能强,而不是因为判别器性能弱。下图展示了判别器和生成器的训练过程。
在 Keras
中,可以通过自定义 train_step
函数实现以上训练过程:
class DCGAN(models.Model):
def __init__(self, discriminator, generator, latent_dim):
super(DCGAN, self).__init__()
self.discriminator = discriminator
self.generator = generator
self.latent_dim = latent_dim
def compile(self, d_optimizer, g_optimizer):
super(DCGAN, self).compile()
# 生成器和判别器的损失函数均为 BinaryCrossentropy
self.loss_fn = losses.BinaryCrossentropy()
self.d_optimizer = d_optimizer
self.g_optimizer = g_optimizer
self.d_loss_metric = metrics.Mean(name="d_loss")
self.d_real_acc_metric = metrics.BinaryAccuracy(name="d_real_acc")
self.d_fake_acc_metric = metrics.BinaryAccuracy(name="d_fake_acc")
self.d_acc_metric = metrics.BinaryAccuracy(name="d_acc")
self.g_loss_metric = metrics.Mean(name="g_loss")
self.g_acc_metric = metrics.BinaryAccuracy(name="g_acc")
@property
def metrics(self):
return [
self.d_loss_metric,
self.d_real_acc_metric,
self.d_fake_acc_metric,
self.d_acc_metric,
self.g_loss_metric,
self.g_acc_metric,
]
def train_step(self, real_images):
# Sample random points in the latent space
batch_size = tf.shape(real_images)[0]
# 从多元标准正态分布中随机采样一批向量
random_latent_vectors = tf.random.normal(
shape=(batch_size, self.latent_dim)
)
# Train the discriminator on fake images
with tf.GradientTape() as gen_tape, tf.GradientTape() as disc_tape:
# 将这些向量通过生成器生成一批图像
generated_images = self.generator(
random_latent_vectors, training=True
)
# 使用判别器预测真实图像与生成图像的真实性概率
real_predictions = self.discriminator(real_images, training=True)
fake_predictions = self.discriminator(generated_images, training=True)
real_labels = tf.ones_like(real_predictions)
real_noisy_labels = real_labels + NOISE_PARAM * tf.random.uniform(
tf.shape(real_predictions)
)
fake_labels = tf.zeros_like(fake_predictions)
fake_noisy_labels = fake_labels - NOISE_PARAM * tf.random.uniform(
tf.shape(fake_predictions)
)
# 判别器损失是真实图像(标签为 1 )和生成图像(标签为 0 )的二元交叉熵的平均值
d_real_loss = self.loss_fn(real_noisy_labels, real_predictions)
d_fake_loss = self.loss_fn(fake_noisy_labels, fake_predictions)
d_loss = (d_real_loss + d_fake_loss) / 2.0
# 生成器损失是判别器对生成图像的预测与标签 1 之间的二元交叉熵
g_loss = self.loss_fn(real_labels, fake_predictions)
gradients_of_discriminator = disc_tape.gradient(d_loss, self.discriminator.trainable_variables)
gradients_of_generator = gen_tape.gradient(g_loss, self.generator.trainable_variables)
# 分别更新判别器和生成器的权重
self.d_optimizer.apply_gradients(
zip(gradients_of_discriminator, discriminator.trainable_variables)
)
self.g_optimizer.apply_gradients(
zip(gradients_of_generator, generator.trainable_variables)
)
# Update metrics
self.d_loss_metric.update_state(d_loss)
self.d_real_acc_metric.update_state(real_labels, real_predictions)
self.d_fake_acc_metric.update_state(fake_labels, fake_predictions)
self.d_acc_metric.update_state(
[real_labels, fake_labels], [real_predictions, fake_predictions]
)
self.g_loss_metric.update_state(g_loss)
self.g_acc_metric.update_state(real_labels, fake_predictions)
return {m.name: m.result() for m in self.metrics}
# Create a DCGAN
dcgan = DCGAN(
discriminator=discriminator, generator=generator, latent_dim=Z_DIM
)
dcgan.compile(
d_optimizer=optimizers.Adam(
learning_rate=LEARNING_RATE, beta_1=ADAM_BETA_1, beta_2=ADAM_BETA_2
),
g_optimizer=optimizers.Adam(
learning_rate=LEARNING_RATE, beta_1=ADAM_BETA_1, beta_2=ADAM_BETA_2
),
)
dcgan.fit(train, epochs=EPOCHS)
在生成器训练期间,由于判别器被冻结,它的权重不会被更新,而生成器的权重将朝着能够更好地生成图像(更有可能骗过判别器的图像)的方向移动(也就是让判别器的预测值接近 1
)。
判别器和生成器不断博弈可能导致 DCGAN
的训练过程不稳定。理想情况下,训练过程能够找到一个平衡点,使得生成器能够从判别器中学习到有意义的信息,并且图像的质量开始提高。经过足够多的 epochs
后,判别器往往会占优势,但生成器在此时可能已经学会生成足够高质量的图像了。
为标签添加一些微小的随机噪音是训练 GAN
时的一个有用技巧。这有助于提高训练过程的稳定性并增强生成的图像。这种标签平滑能够使判别器处理更具挑战性的任务,而不至于令生成器无法训练。
通过观察训练过程中生成器的生成图像,可以明显看出生成器生成的图像越来越接近从训练集中采样的图像。
可以看到,神经网络有能力将随机噪音转化为有意义的内容,同时,除了原始图像外,我们没有为模型提供任何额外的特征,因此它必须独自学习高级特征,例如绘制阴影、立方体和圆圈。
成功的生成模型还需要满足一个要求,即它不能仅仅是复制训练集中已有的图像。为了测试这一点,我们可以从训练数据集中找到与生成图像最接近的图像,可以使用 L1
距离度量图像间的距离:
def compare_images(img1, img2):
return np.mean(np.abs(img1 - img2))
下图展示了一些生成图像在训练集中与之最接近的观测样本。可以看出,虽然生成的图像和训练集之间存在一定程度的相似性,但它们并非完全相同。这表明生成器已经学习到训练数据集中的高级特征,并能够生成与训练数据集中图像不同的样本。
4. GAN 训练技巧
虽然 GAN
是生成模型领域的重大突破,但也存在训练难度较大的问题。在本节中,我们将探讨训练 GAN
时遇到的一些最常见的问题,以及相应的解决方案。
4.1 判别器强于生成器
如果判别器变得过强,损失函数的信号就会变得过弱,无法推动生成器产生有意义的改进。在最糟糕的情况下,判别器完全学会了区分真实图像和虚假图像,导致梯度完全消失,从而无法进行任何训练。
如果判别器的损失函数失控,就需要弱判别器:
- 增加判别器中
Dropout
层的丢弃率,减少信息通过网络的量 - 降低判别器的学习率
- 减少判别器中的卷积滤波器数量
- 在训练判别器时,为标签添加噪声
- 在训练判别器时,随机将一些图像的标签反转(从
0
变为1
,或从1
变为0
)
4.2 生成器强于判别器
如果判别器不够强大,生成器会找到轻松欺骗判别器的方法,并只生成一小部分几乎相同的图像样本,这称为模式坍塌 (Mode Collapse
)。
假设我们在不更新判别器的情况下对生成器进行了多次训练。生成器倾向于找到一个总是能够欺骗判别器的单个观测样本(也称为模式),并开始将潜在输入空间中的每个点映射到这个观测样本图像。此外,损失函数的梯度会趋近于零,因此无法从此状态恢复。
即使我们随后尝试重新训练判别器,以阻止其被这一个模式欺骗,生成器仍会找到另一个模式判别器的模式,因为它已经没有多样化输出的动力。
如果生成器出现模式崩溃的问题,可以尝试使用与上一小节相反的方法来加强判别器。此外,也可以尝试减小两个网络的学习率并增加批大小。
4.3 信息量不足
由于深度学习模型被编译为最小化损失函数,因此可能自然地认为生成器的损失函数越小,生成的图像质量就越好。然而,由于生成器只针对当前的判别器进行评估,而判别器在不断改进,我们无法比较在训练过程中不同时间点评估的损失函数。实际上,在训练过程中,尽管图像质量明显提高,生成器的损失也可能会随着时间的推移而增加。由于生成器损失与图像质量之间缺乏相关性,有时使得 GAN
的训练难以监控。
4.4 超参数
在 GAN
训练过程中,我们可以看到,即使是简单的 GAN
也有大量需要调整的超参数 (Hyperparameter
)。除了判别器和生成器的整体架构外,还有一些参数控制着批归一化、Dropout
、学习率、激活层、卷积核大小、步长、批大小和潜空间大小等。GAN
对这些参数的微小变化非常敏感,因此找到一组有效的参数通常需要反复实验,而没有一套既定的准则。
因此了解 GAN
的内部工作原理并知道如何解读损失函数至关重要,只有这样才能合理的进行超参数调整,以提高模型的稳定性。
小结
在本节中,我们首先介绍了生成对抗网络 (Generative Adversarial Network
, GAN
) 的基本原理,并学习了如何训练 DCGAN
生成玩具积木图像,GAN
能够学会以图像的形式真实地表示 3D
对象,包括阴影、形状和纹理;还探讨了 GAN
训练过程中可能会遇到的问题,及相应的解决方法和训练技巧。
系列链接
AIGC实战——生成模型简介
AIGC实战——深度学习 (Deep Learning, DL)
AIGC实战——卷积神经网络(Convolutional Neural Network, CNN)
AIGC实战——自编码器(Autoencoder)
AIGC实战——变分自编码器(Variational Autoencoder, VAE)
AIGC实战——使用变分自编码器生成面部图像