使用 PyTorch 学习生成式人工智能——使用生成对抗网络进行图像生成

172 阅读39分钟

本章内容包括:

  • 通过镜像判别器网络的步骤设计生成器
  • 2D卷积操作如何作用于图像
  • 2D转置卷积操作如何在输出值之间插入空隙,生成更高分辨率的特征图
  • 构建并训练生成对抗网络(GAN),生成灰度和彩色图像

在第三章中,你成功生成了指数增长曲线和所有数值为5的倍数的整数序列。现在你已理解生成对抗网络(GAN)的工作原理,可以将这些技能应用于生成其他多种形式的内容,比如高分辨率彩色图像和逼真音乐。然而,实际操作可能并非易事(俗话说:“细节决定成败”)。比如,生成器究竟如何凭空“创造”出真实感的图像?本章将解答这一问题。

生成器从零生成图像的常用方法是镜像判别器网络的结构。

本章第一个项目中,你的目标是生成衣物的灰度图像,比如大衣、衬衫、凉鞋等。设计生成器网络时,你将学习镜像判别器网络中的层结构。本项目中,生成器和判别器均只使用密集层(Dense Layers)。密集层中的每个神经元都与前后层的所有神经元相连,因此也称为全连接层。

本章第二个项目目标是生成高分辨率的动漫脸部彩色图像。与第一个项目相似,生成器镜像判别器的结构来生成图像。但高分辨率彩色图像比低分辨率灰度图像包含更多像素,如果仅用密集层,模型参数将大幅增加,导致学习效率低下,训练缓慢。

因此,我们采用卷积神经网络(CNN)。在CNN中,每层神经元仅连接输入的局部区域,这种局部连接减少了参数数量,使网络更高效。CNN相比相似规模的全连接网络参数更少,训练更快,计算成本更低。且CNN擅长捕捉图像数据中的空间层次结构,因为它们将图像视为多维对象,而非一维向量。

为准备第二个项目,我们将介绍卷积操作的工作原理,说明它如何对输入图像进行下采样并提取空间特征。你将学习滤波器大小、步幅(stride)、零填充(zero-padding)等概念,以及它们对卷积层下采样程度的影响。

判别器网络使用卷积层,生成器则通过使用转置卷积层(又称反卷积或上采样层)来镜像这些层。你将学习转置卷积层如何用于上采样,生成高分辨率特征图。

总结来说,本章你将学习如何镜像判别器的步骤从零生成图像,以及卷积层和转置卷积层的工作机制。学完本章后,你将在书中后续章节使用卷积层和转置卷积层,生成更高分辨率的图像(例如训练CycleGAN时将金发转换成黑发的特征迁移,或变分自编码器VAE生成高分辨率人脸图像)。

4.1 使用GAN生成服装灰度图像

本章第一个项目的目标是训练一个模型,生成诸如凉鞋、T恤、大衣和包包等服装的灰度图像。
在使用GAN生成图像时,通常从获取训练数据开始。然后从零创建一个判别器网络,设计生成器网络时镜像判别器网络的结构。最后,训练GAN,并使用训练好的模型生成图像。下面我们通过一个简单项目,演示如何生成服装灰度图像。

4.1.1 训练样本与判别器

准备训练数据的步骤与第二章类似,这里略有不同,后面我会重点说明。为了节省时间,这里跳过第二章中你已经见过的步骤,推荐你参考本书GitHub仓库中本章节的Jupyter Notebook(github.com/markhliu/DG…)来创建批量数据迭代器。

训练集中有6万张图像。第二章中,我们将训练集进一步划分为训练集和验证集,通过验证集的损失判断参数是否收敛以停止训练。但GAN的训练方法与传统的监督学习模型不同(如你在第二章看到的分类模型)。因为生成样本的质量随着训练持续提升,判别器的任务会越来越困难,判别器的损失并不是衡量模型质量的好指标。
通常评估GAN性能的方法是通过视觉检查生成图像的质量与真实感。也可以将生成样本与训练样本进行比较,使用诸如Inception Score的方法评估GAN性能(详见Ali Borji,2018年的综述论文《Pros and Cons of GAN Evaluation Measures》,arxiv.org/abs/1802.03…)。不过,这些指标也存在缺陷(参见Shane Barratt和Rishi Sharma,2018年《A Note on the Inception Score》,arxiv.org/abs/1801.01…),说明Inception Score在比较模型时指导意义有限。本章中,我们将通过定期的视觉检查来判断生成样本的质量并决定何时停止训练。
判别器网络是一个二分类器,类似于第二章中讨论的服装类别二分类器。判别器负责判定样本是真实还是伪造。

我们用PyTorch创建如下判别器神经网络D:

import torch
import torch.nn as nn

device = "cuda" if torch.cuda.is_available() else "cpu"
D = nn.Sequential(
    nn.Linear(784, 1024),          # ①
    nn.ReLU(),
    nn.Dropout(0.3),
    nn.Linear(1024, 512),
    nn.ReLU(),
    nn.Dropout(0.3),
    nn.Linear(512, 256),
    nn.ReLU(),
    nn.Dropout(0.3),
    nn.Linear(256, 1),    
    nn.Sigmoid()).to(device)       # ②

① 第一层全连接层有784个输入和1024个输出。
② 最后一层全连接层有256个输入和1个输出。

输入尺寸是784,因为训练集中的每张灰度图像大小为28×28像素。由于密集层只接受一维输入,我们先将图像展平后再送入模型。输出层只有一个神经元:判别器D的输出是一个单值,使用sigmoid激活函数将其压缩到[0, 1]区间,可被解释为样本为真实的概率p,补充概率1-p则为样本为伪造的概率。

练习4.1
修改判别器D,使得前三层的输出神经元数分别为1000、500和200,替换原先的1024、512和256。注意保证每层输出的神经元数与下一层输入神经元数匹配。

4.1.2 设计生成器生成灰度图像

判别器网络相对容易构建,而如何设计生成器以生成逼真图像则是另一回事。常见做法是镜像判别器的层结构设计生成器,示例如下:

G = nn.Sequential(
    nn.Linear(100, 256),         # ①
    nn.ReLU(),
    nn.Linear(256, 512),         # ②
    nn.ReLU(),
    nn.Linear(512, 1024),        # ③
    nn.ReLU(),
    nn.Linear(1024, 784),        # ④
    nn.Tanh()).to(device)        # ⑤

① 生成器第一层对应判别器最后一层。
② 生成器第二层对应判别器倒数第二层(输入输出数量互换)。
③ 生成器第三层对应判别器倒数第三层。
④ 生成器最后一层对应判别器第一层。
⑤ 使用Tanh激活函数,将输出压缩到[-1, 1]区间,与图像像素值范围相同。

图4.1展示了GAN中生成器和判别器网络的架构示意图,用于生成服装灰度图像。图右上角显示,训练集中的一张展平后的灰度图(784像素)依次经过判别器的四个密集层,输出该图像为真实的概率。生成器则使用相同的四层密集层但顺序相反:从二维潜在空间随机采样一个100维噪声向量(图左下),依次经过四层密集层,输出一个784维张量,可重新塑造成28×28的灰度图(图左上)。

image.png

图4.1 通过镜像判别器网络的层设计生成器网络以创建服装图像。图的右侧显示判别器网络,包含四个密集层。为了设计一个能够凭空生成服装图像的生成器,我们镜像判别器网络的层结构。具体来说,如图左半部分所示,生成器包含四个相似的密集层,但顺序相反:生成器的第一层镜像判别器的最后一层,生成器的第二层镜像判别器的倒数第二层,以此类推。此外,在前三层中,判别器中输入和输出的数量被反转,作为生成器中输出和输入的数量。

图4.1的左侧是生成器网络,右侧是判别器网络。如果比较这两个网络,你会发现生成器镜像了判别器使用的层。具体来说,生成器中有四个类似的密集层,但顺序相反:生成器第一层镜像判别器最后一层,生成器第二层镜像判别器倒数第二层,以此类推。生成器的输出数量为784,经过Tanh()激活后数值在-1到1之间,与判别器网络的输入相匹配。

练习4.2
修改生成器G,使其前三层的输出神经元数量分别为1000、500和200,替换原先的1024、512和256。确保修改后的生成器镜像练习4.1中修改后的判别器网络层结构。

与第三章中见到的GAN模型类似,判别器D执行的是二分类问题,因此损失函数采用二元交叉熵损失函数。我们为判别器和生成器都使用Adam优化器,学习率设置为0.0001:

loss_fn = nn.BCELoss()
lr = 0.0001
optimD = torch.optim.Adam(D.parameters(), lr=lr)
optimG = torch.optim.Adam(G.parameters(), lr=lr)

接下来,我们将使用训练数据集中的服装图像训练刚刚创建的GAN模型。

4.1.3 训练GAN生成服装图像

训练过程与第3章中训练GAN生成指数增长曲线或生成全部为5的倍数的数字序列类似。
不同于第3章,我们将完全依赖视觉检查来判断模型是否训练充分。为此,我们定义了一个 see_output() 函数,定期可视化生成器生成的假图像。
注意:感兴趣的读者可以查看这个GitHub仓库,了解如何用PyTorch实现Inception Score来评估GAN:github.com/sbarratt/in… 。不过,该仓库并不推荐使用Inception Score来评估生成模型,因为它效果有限。

代码清单4.2 定义用于可视化生成服装图像的函数

import matplotlib.pyplot as plt
  
def see_output():
    noise = torch.randn(32, 100).to(device=device)           # ① 生成32张假图像
    fake_samples = G(noise).cpu().detach()                    # ①
    plt.figure(dpi=100, figsize=(20, 10))
    for i in range(32):
        ax = plt.subplot(4, 8, i + 1)                         # ② 以4×8的网格绘制
        img = (fake_samples[i] / 2 + 0.5).reshape(28, 28)     # ③ 显示第i张图像
        plt.imshow(img)
        plt.xticks([])
        plt.yticks([])
    plt.show()
    
see_output()                                                # ④ 调用函数,训练前可视化生成图像

运行上述代码单元后,你将看到32张图像,看起来像电视屏幕上的雪花噪点,如图4.2所示。它们根本不像服装图像,因为我们还没有训练生成器。

image.png

图4.2 显示了训练前GAN模型生成的服装图像输出。由于模型尚未训练,生成的图像与训练集中的图像完全不同。
为了训练GAN模型,我们定义了几个函数:train_D_on_real()、train_D_on_fake()和train_G(),它们与第3章中定义的函数类似。你可以前往本章的Jupyter Notebook(位于本书的GitHub仓库)查看我们所做的一些细微修改。

现在我们准备训练模型。我们遍历训练数据集中的所有批次数据。对于每个批次数据,先用真实样本训练判别器;之后生成器创建一批假样本,再用这些假样本训练判别器;最后生成器再次生成一批假样本,这次用来训练生成器本身。我们训练模型共50个epoch,具体代码如下:

代码清单4.3 训练GAN生成服装图像

python
复制
for i in range(50):    
    gloss = 0
    dloss = 0
    for n, (real_samples, _) in enumerate(train_loader):
        loss_D = train_D_on_real(real_samples)            # ① 用真实样本训练判别器
        dloss += loss_D
        loss_D = train_D_on_fake()                         # ② 用假样本训练判别器
        dloss += loss_D
        loss_G = train_G()                                 # ③ 训练生成器
        gloss += loss_G
    gloss = gloss / n
    dloss = dloss / n    
    if i % 10 == 9:
        print(f"at epoch {i+1}, dloss: {dloss}, gloss {gloss}")
        see_output()                                      # ④ 每10个epoch可视化一次生成的样本

如果使用GPU训练,整个训练过程大约需要10分钟;否则,训练时间会根据你的电脑硬件配置不同,可能需要一个小时左右。你也可以从我的网站下载已经训练好的模型:gattonweb.uky.edu/faculty/liu… ,下载后解压即可。

每训练10个epoch,你都可以可视化生成的服装图像,如图4.3所示。仅经过10个epoch的训练,模型已经能生成清晰可辨的服装图像:你可以看出它们是什么。例如,图4.3第一排的前三个图像明显分别是外套、连衣裙和一条裤子。随着训练的进行,生成图像的质量会越来越好。

image.png

图4.3 经过10个epoch训练后,图像GAN模型生成的服装图像。

和所有GAN模型一样,我们舍弃判别器,保存训练好的生成器,以便后续生成样本:

scripted = torch.jit.script(G) 
scripted.save('files/fashion_gen.pt') 

这样,生成器就保存在本地文件夹中了。使用生成器时,我们加载保存的模型:

new_G = torch.jit.load('files/fashion_gen.pt', map_location=device)
new_G.eval()

生成器现在已经加载完成,我们可以用它来生成服装图像:

noise = torch.randn(32, 100).to(device=device)
fake_samples = new_G(noise).cpu().detach()
for i in range(32):
    ax = plt.subplot(4, 8, i + 1)
    plt.imshow((fake_samples[i] / 2 + 0.5).reshape(28, 28))
    plt.xticks([])
    plt.yticks([])
plt.subplots_adjust(hspace=-0.6)
plt.show()

生成的服装图像如图4.4所示。正如你所见,生成的服装图像与训练集中的图像非常接近。

image.png

图4.4 经过训练的图像GAN模型(50个epoch后)生成的服装图像。

现在你已经学会了如何使用GAN生成灰度图像,接下来章节中你将学习如何使用深度卷积GAN(DCGAN)生成高分辨率彩色图像。

4.2 卷积层

为了生成高分辨率彩色图像,我们需要比简单的全连接神经网络更复杂的技术。具体来说,我们将使用卷积神经网络(CNN),它们对于处理具有网格状拓扑结构的数据(如图像)特别有效。

CNN与全连接(密集)层有几个不同点。首先,在CNN中,每个层的神经元只与输入的一小块区域连接。这是基于图像数据中局部像素群更可能相互关联的理解。局部连接减少了参数数量,使网络更高效。其次,CNN采用共享权重的概念——相同的权重被用于输入的不同区域。这类似于将一个滤波器滑过整个输入空间,这个滤波器能够检测特定的特征(如边缘或纹理),无论其在输入中的位置,从而具备平移不变性。

由于这种结构,CNN在图像处理方面更高效。它们所需的参数比相似规模的全连接网络更少,因此训练速度更快,计算成本更低。同时,CNN通常更有效地捕捉图像数据中的空间层级结构。

卷积层和转置卷积层是CNN中两个基本的构建模块,常用于图像处理和计算机视觉任务。它们的目的和特点不同:卷积层用于特征提取,通过对输入数据应用一组可学习的滤波器(也称核)来检测不同空间尺度上的模式和特征,这对于捕捉输入数据的层级表示至关重要;而转置卷积层则用于上采样或生成高分辨率的特征图。

本节你将学习卷积操作的工作原理,以及卷积核大小、步幅和零填充如何影响卷积操作。

4.2.1 卷积操作如何工作?

卷积层使用滤波器提取输入数据的空间模式。一个卷积层能够自动检测大量模式并将它们与目标标签关联,因此卷积层常用于图像分类任务。

卷积操作是将滤波器应用于输入图像以生成特征图的过程。其步骤包括滤波器与输入图像对应元素的逐元素相乘并求和。滤波器的权重在滑动滤波器扫描输入图像的不同区域时保持不变。图4.5展示了卷积操作的数值示例:左列为输入图像,中间为一个2×2的滤波器,右列展示了卷积操作——滤波器在输入图像上滑动,逐元素相乘并求和的过程。

image.png

图4.5 展示了步幅为1且无填充时卷积操作的数值示例。

为了深入理解卷积操作的具体过程,我们将在PyTorch中实现卷积操作,方便你验证图4.5中所示的数值。首先,创建一个PyTorch张量表示图中的输入图像:

img = torch.Tensor([[1,1,1],
                    [0,1,2],
                    [8,7,6]]).reshape(1,1,3,3)    ①

① 图像的形状为 (1, 1, 3, 3),分别代表批次中图像数量、颜色通道数、图像高度和图像宽度。

图像被重塑为维度为 (1, 1, 3, 3),表示批次中只有一张图像,且只有一个颜色通道。图像的高和宽均为3像素。

接下来,表示图4.5第二列所示的2×2滤波器,在PyTorch中创建一个二维卷积层:

conv=nn.Conv2d(in_channels=1,
               out_channels=1,
               kernel_size=2, 
               stride=1)                             ①
sd=conv.state_dict()                              ②
print(sd)

① 初始化一个二维卷积层。

② 提取该层随机初始化的权重和偏置。

二维卷积层需要多个参数。in_channels 是输入图像的通道数,灰度图为1,彩色图为3(红绿蓝RGB三个通道)。out_channels 是卷积层输出的通道数,取决于你想从图像中提取多少特征。kernel_size 控制滤波器大小,如 kernel_size=3 表示滤波器为 3×3,kernel_size=4 表示为 4×4。这里我们设置为2,即滤波器为2×2。

二维卷积层还有其他可选参数。stride 表示滤波器每次移动多少像素,默认1。stride 越大,下采样越多。padding 表示在输入图像四周填充多少行0,默认0。bias 表示是否添加可学习的偏置项,默认True。

上面创建的卷积层有1个输入通道,1个输出通道,滤波器大小为2×2,步幅为1。权重和偏置初始化为随机值。输出类似如下:

OrderedDict([('weight', tensor([[[[ 0.3823,  0.4150],
                                  [-0.1171,  0.4593]]]])), ('bias', tensor([-0.1096]))])

为了更容易理解,我们将权重和偏置替换为整数:

weights={'weight':torch.tensor([[[[1,2],
                                  [3,4]]]]), 'bias':torch.tensor([0])}          ①
for k in sd:
    with torch.no_grad():
        sd[k].copy_(weights[k])                  ②
print(conv.state_dict())                         ③

① 选定权重和偏置。

② 使用选定的权重和偏置替换卷积层中的原有值。

③ 打印替换后的权重和偏置。

因为不需要学习这些参数,我们用 torch.no_grad() 关闭梯度计算,减少内存消耗并加快计算速度。此时卷积层的权重和偏置与图4.5中的数值一致。输出如下:

OrderedDict([('weight', tensor([[[[1., 2.],
                                  [3., 4.]]]])), ('bias', tensor([0.]))])

接下来,将上述卷积层应用于之前的3×3输入图像:

output = conv(img)
print(output)

输出为:

tensor([[[[ 7., 14.],
          [54., 50.]]]], grad_fn=<ConvolutionBackward0>)

输出维度为 (1, 1, 2, 2),包含4个值:7,14,54 和 50。这些数值与图4.5中的对应数值完全一致。

那么,卷积层是如何通过滤波器生成上述输出的呢?下面详细解释。

输入图像是3×3矩阵,滤波器是2×2矩阵。滤波器第一次覆盖输入图像左上角的4个像素,值为:

[[1, 1],
 [0, 1]]

滤波器的权重为:

[[1, 2],
 [3, 4]]

卷积操作是将滤波器与覆盖区域对应元素相乘后求和,即:

1*1 + 1*2 + 0*3 + 1*4 = 7

这解释了为何输出左上角是7。同理,滤波器滑动至输入图像右上角覆盖区域:

[[1, 1],
 [1, 2]]

对应计算为:

1*1 + 1*2 + 1*3 + 2*4 = 14

这解释了为何输出右上角是14。

练习4.3

当滤波器应用于输入图像右下角时,覆盖区域的数值是什么?解释为何输出右下角是50。

4.2.2 步幅(stride)和填充(padding)如何影响卷积操作?

步幅和零填充是卷积操作中两个重要的概念,它们在决定输出特征图的尺寸以及滤波器与输入数据交互的方式上起着关键作用。

步幅指的是滤波器在输入图像上每次移动的像素数。当步幅为1时,滤波器每次移动1个像素。步幅越大,滤波器在滑动时跳过的像素就越多。增加步幅会减少输出特征图的空间尺寸。

零填充是指在输入图像的边缘添加一层或多层0,然后再进行卷积操作。零填充可以控制输出特征图的空间尺寸。如果没有填充,输出的尺寸会比输入小。通过添加填充,可以保持输入尺寸不变。

下面用一个例子来说明步幅和填充的作用。以下代码重新定义了一个二维卷积层:

conv=nn.Conv2d(in_channels=1,
               out_channels=1,
               kernel_size=2, 
               stride=2,                           ①
               padding=1)                          ②
sd=conv.state_dict()
for k in sd:
    with torch.no_grad():
        sd[k].copy_(weights[k])
output = conv(img)
print(output)

① 将步幅从1改为2。

② 将填充从0改为1。

输出结果是:

tensor([[[[ 4.,  7.],
          [32., 50.]]]], grad_fn=<ConvolutionBackward0>)

参数 padding=1 表示在输入图像四周各添加一层0,因此填充后的图像尺寸从 3×3 变成了 5×5。

当滤波器扫描填充后的图像时,第一次覆盖的是左上角的区域,值为:

[[0, 0],
 [0, 1]]

滤波器权重为:

[[1, 2],
 [3, 4]]

因此,左上角的输出计算为:

0×1 + 0×2 + 0×3 + 1×4 = 4

这解释了为什么输出的左上角值是4。类似地,当滤波器向下滑动两像素到图像左下角时,覆盖区域为:

[[0, 0],
 [0, 8]]

输出为:

0×1 + 0×2 + 0×3 + 8×4 = 32

这解释了为什么输出的左下角值是32。

4.3 转置卷积和批量归一化

转置卷积层也称为反卷积层或上采样层,主要用于上采样或生成高分辨率的特征图。它们常用于生成模型,如生成对抗网络(GANs)和变分自编码器(VAEs)中。

转置卷积层对输入数据应用滤波器,但与标准卷积不同的是,它通过在输出值之间插入空隙来增加空间尺寸,从而有效地“放大”特征图。这个过程生成了更高分辨率的特征图。转置卷积层有助于提高空间分辨率,这在图像生成中非常有用。

步幅(stride)同样可以用于转置卷积层,以控制上采样的程度。步幅值越大,转置卷积层对输入数据的上采样越多。

二维批量归一化是一种在神经网络中使用的技术,特别是在卷积神经网络(CNNs)中,用于稳定和加速训练过程。它解决了多个问题,包括饱和、梯度消失和梯度爆炸,这些都是深度学习中常见的挑战。在本节中,你将通过一些示例深入了解其工作原理。你将在下一节创建生成高分辨率彩色图像的GAN时使用它。

深度学习中的梯度消失与梯度爆炸

梯度消失问题发生在深度神经网络中,当损失函数对网络参数的梯度在反向传播过程中变得极小。这导致参数更新非常缓慢,尤其影响网络早期层的学习过程。相反,梯度爆炸问题发生在这些梯度变得过大时,导致参数更新不稳定,模型参数出现震荡或发散到极大值。这两种问题都会阻碍深度神经网络的有效训练。

4.3.1 转置卷积层如何工作?

与卷积层相反,转置卷积层通过使用核(即滤波器)对图像进行上采样并填补空隙,从而生成特征并提升分辨率。转置卷积层的输出通常比输入更大。因此,转置卷积层是生成高分辨率图像的重要工具。

为了准确展示二维转置卷积操作的工作原理,我们通过一个简单的示例和图示来说明。假设你有一个非常小的2×2输入图像,如图4.6的左侧列所示。

image.png

输入图像的值如下:

img = torch.Tensor([[1,0],
                    [2,3]]).reshape(1,1,2,2)

你希望对该图像进行上采样,使其拥有更高的分辨率。你可以在 PyTorch 中创建一个二维转置卷积层:

transconv=nn.ConvTranspose2d(in_channels=1,
            out_channels=1,
            kernel_size=2, 
            stride=2)                            ①
sd=transconv.state_dict()
weights={'weight':torch.tensor([[[[2,3],
   [4,5]]]]), 'bias':torch.tensor([0])}
for k in sd:
    with torch.no_grad():
        sd[k].copy_(weights[k])                  ②

① 创建一个具有 1 个输入通道、1 个输出通道、卷积核大小为 2、步幅为 2 的二维转置卷积层
② 用手动指定的权重和偏置替换该转置卷积层中的随机初始化权重和偏置

该二维转置卷积层具有一个输入通道和一个输出通道,卷积核大小为 2×2,步幅为 2。图4.6的第二列展示了这个2×2的滤波器。我们用手动选取的整数替换了层中随机初始化的权重和偏置,以便更容易跟踪计算过程。代码中的 state_dict() 方法返回深度神经网络中的参数。

当这个转置卷积层应用于上面提到的 2×2 图像时,输出是什么?让我们来看一下:

transoutput = transconv(img)
print(transoutput)

输出为:

tensor([[[[ 2.,  3.,  0.,  0.],
          [ 4.,  5.,  0.,  0.],
          [ 4.,  6.,  6.,  9.],
          [ 8., 10., 12., 15.]]]], grad_fn=<ConvolutionBackward0>)

输出的形状为 (1, 1, 4, 4),表示我们已经将 2×2 图像上采样成了 4×4 图像。接下来我们详细解释转置卷积层是如何通过滤波器生成上述输出的。

输入图像是一个 2×2 的矩阵,滤波器也是一个 2×2 的矩阵。当滤波器应用于图像时,图像中的每个元素都会与滤波器相乘,并将结果累加到输出中。图像左上角的值是 1,我们将其与滤波器中的值 [[2, 3], [4, 5]] 相乘,得到输出矩阵左上角的四个值 [[2, 3], [4, 5]],如图4.6右上角所示。类似地,图像左下角的值是 2,我们将其与滤波器的值相乘,得到输出矩阵左下角的四个值 [[4, 6], [8, 10]]

练习 4.4
如果图像的值是 [[10, 10], [15, 20]],并且应用相同的二维转置卷积层 transconv(权重为 [[2, 3], [4, 5]],卷积核大小为 2,步幅为 2),输出会是什么?

4.3.2 批归一化(Batch Normalization)

二维批归一化是现代深度学习框架中的一种标准技术,已成为有效训练深度神经网络的关键组成部分。在本书后续章节中你会经常遇到它。

在二维批归一化中,对于每个特征通道独立进行归一化处理,通过调整和缩放该通道中的数值,使其均值为0,方差为1。特征通道是CNN中多维张量的一个维度,用于表示输入数据的不同方面或特征。例如,它们可以代表颜色通道,如红色、绿色或蓝色。归一化保证网络深层各层输入分布的稳定性。这种稳定性源于归一化过程减少了内部协变量偏移(internal covariate shift),即因底层权重更新而引起的网络激活分布变化。归一化还帮助解决梯度消失或爆炸问题,通过保持输入在合适范围内,防止梯度过小(消失)或过大(爆炸)。¹

二维批归一化的工作流程如下:对每个特征通道,首先计算该通道所有样本的均值和方差;然后用先前计算得到的均值和方差对通道内的每个数值进行归一化处理(即用该数值减去均值,再除以标准差)。这样每个通道经过归一化后,其数值均值为0,标准差为1,有助于稳定和加速训练过程。批归一化还能保持反向传播过程中的梯度稳定,从而促进深度神经网络的训练。

下面用一个具体例子来演示二维批归一化的工作原理。

假设你有一个大小为64×64的三通道输入图像。你将该输入通过一个输出通道为3的二维卷积层:

torch.manual_seed(42)                          ①
img = torch.rand(1,3,64,64)                    ②
conv = nn.Conv2d(in_channels=3,
            out_channels=3,
            kernel_size=3, 
            stride=1,
            padding=1)                         ③
out=conv(img)                                  ④
print(out.shape)

① 固定随机种子,保证结果可复现
② 创建一个三通道输入
③ 创建一个二维卷积层
④ 将输入传入卷积层

输出为:

torch.Size([1, 3, 64, 64])

我们创建了一个三通道输入,并通过一个输出通道数为3的二维卷积层处理。处理后的输出有3个通道,每个通道大小为64×64像素。

接下来查看三个输出通道中像素的均值和标准差:

for i in range(3):
    print(f"mean in channel {i} is", out[:,i,:,:].mean().item())
    print(f"std in channel {i} is", out[:,i,:,:].std().item())

输出结果:

mean in channel 0 is -0.3766776919364929
std in channel 0 is 0.17841289937496185
mean in channel 1 is -0.3910464942455292
std in channel 1 is 0.16061744093894958
mean in channel 2 is 0.39275866746902466
std in channel 2 is 0.18207983672618866

每个通道的像素均值并非0,标准差也不是1。

现在进行二维批归一化:

norm=nn.BatchNorm2d(3)
out2=norm(out)
print(out2.shape)
for i in range(3):
    print(f"mean in channel {i} is", out2[:,i,:,:].mean().item())
    print(f"std in channel {i} is", out2[:,i,:,:].std().item())

得到输出:

torch.Size([1, 3, 64, 64])
mean in channel 0 is 6.984919309616089e-09
std in channel 0 is 0.9999650120735168
mean in channel 1 is -5.3085386753082275e-08
std in channel 1 is 0.9999282956123352
mean in channel 2 is 9.872019290924072e-08
std in channel 2 is 0.9999712705612183

此时,每个通道的像素均值几乎为0(或极其接近0),标准差接近1。

这就是批归一化的作用:对每个特征通道的观测值进行归一化,使每个通道的值均值为0,标准差为1,从而稳定训练过程。

4.4 动漫人脸的彩色图像生成

在这个第二个项目中,你将学习如何创建高分辨率的彩色图像。本项目的训练步骤与第一个项目类似,不同之处在于训练数据是动漫人脸的彩色图像。此外,判别器和生成器神经网络也更加复杂。我们将在这两个网络中使用二维卷积层和二维转置卷积层。

4.4.1 下载动漫人脸数据

你可以从 Kaggle(mng.bz/1a9R)下载训练数据,数据集包含63,632张动漫人脸彩色图像。你需要先注册一个免费的 Kaggle 账号登录。解压缩下载的压缩包并将文件放到电脑的某个文件夹中。比如,我将所有文件放在电脑的 /files/anime/ 目录下,所有动漫人脸图像都在 /files/anime/images/。

定义路径名,方便后续在 PyTorch 中加载图像:

anime_path = r"files/anime"

根据你电脑上存放图像的位置修改路径名。注意,ImageFolder() 类会根据图像所在目录名来识别图像所属的类别,因此最终的 /images/ 目录不包括在我们定义的 anime_path 中。

接下来,使用 Torchvision 的 ImageFolder() 类加载数据集:

from torchvision import transforms as T
from torchvision.datasets import ImageFolder

transform = T.Compose([
    T.Resize((64, 64)),              # ① 将图像大小调整为64×64像素
    T.ToTensor(),                   # ② 转换图像为PyTorch张量
    T.Normalize([0.5, 0.5, 0.5],   # ③ 在三个颜色通道上进行归一化,将像素值范围标准化到[-1, 1]
                [0.5, 0.5, 0.5])
])
train_data = ImageFolder(root=anime_path, transform=transform)  # ④ 加载数据并应用变换

加载本地文件夹中的图像时,执行了三种不同的变换。首先,将所有图像调整为64×64像素;其次,使用 ToTensor() 将图像转换为数值在[0,1]区间的PyTorch张量;最后,用 Normalize() 将像素值先减去0.5,再除以0.5,使数据范围变为[-1,1]。

接下来,将训练数据分批处理:

from torch.utils.data import DataLoader

batch_size = 128
train_loader = DataLoader(dataset=train_data, batch_size=batch_size, shuffle=True)

训练数据集现在以批次形式存在,批量大小为128。

4.4.2 PyTorch中通道优先的彩色图像格式

PyTorch 处理彩色图像时采用通道优先(channels-first)格式,即图像形状为(通道数,高度,宽度)。而其他Python库如 TensorFlow 或 Matplotlib 通常采用通道最后(channels-last)格式,即图像形状为(高度,宽度,通道数)。

来看数据集中的一张示例图像,并打印其形状:

image0, _ = train_data[0]
print(image0.shape)

输出结果:

torch.Size([3, 64, 64])

第一张图像形状为 3×64×64,表示图像有3个颜色通道(RGB),高度和宽度均为64像素。

当我们用 Matplotlib 绘制图像时,需要用 PyTorch 的 permute() 方法将图像转为通道最后格式:

import matplotlib.pyplot as plt

plt.imshow(image0.permute(1, 2, 0) * 0.5 + 0.5)
plt.show()

注意要先将像素值从[-1,1]转换回[0,1],因此先乘以0.5再加0.5。运行上述代码后,你会看到一张动漫人脸图像。

接着定义一个函数 plot_images(),用来展示32张图像,排列为4行8列:

def plot_images(imgs):
    for i in range(32):
        ax = plt.subplot(4, 8, i + 1)
        plt.imshow(imgs[i].permute(1, 2, 0) / 2 + 0.5)
        plt.xticks([])
        plt.yticks([])
    plt.subplots_adjust(hspace=-0.6)
    plt.show()

imgs, _ = next(iter(train_loader))
plot_images(imgs)

① 定义一个函数用于可视化32张图像
② 将图像布局为4×8的网格
③ 获取一批图像
④ 调用函数展示图像

运行后,你会看到如图4.7所示,32张动漫人脸图像以4×8网格形式显示。

image.png

4.5 深度卷积生成对抗网络(DCGAN)

本节中,你将创建一个DCGAN模型,用于训练生成动漫人脸图像。与之前见过的GAN模型一样,DCGAN由判别器网络和生成器网络组成。但这次网络结构更加复杂,我们将使用卷积层、转置卷积层和批量归一化层。

我们先从判别器网络开始讲解,随后介绍生成器网络如何镜像判别器网络的层,生成逼真的彩色图像。然后,你将使用本章前面准备的数据训练模型,并用训练好的模型生成全新的动漫人脸图像。

4.5.1 构建DCGAN

和之前见过的GAN模型一样,判别器是一个二分类器,用于区分样本是真实还是生成的。但不同于之前使用的网络,这次判别器将使用卷积层和批量归一化。高分辨率彩色图像含有大量参数,如果仅用全连接层,训练效果会很差。下面列出了判别器网络结构。

代码示例 4.4 DCGAN中的判别器

import torch.nn as nn
import torch

device = "cuda" if torch.cuda.is_available() else "cpu"

D = nn.Sequential(
    nn.Conv2d(3, 64, 4, 2, 1, bias=False),           # ①
    nn.LeakyReLU(0.2, inplace=True),                 # ②
    nn.Conv2d(64, 128, 4, 2, 1, bias=False),
    nn.BatchNorm2d(128),                             # ③
    nn.LeakyReLU(0.2, inplace=True),
    nn.Conv2d(128, 256, 4, 2, 1, bias=False),
    nn.BatchNorm2d(256),
    nn.LeakyReLU(0.2, inplace=True),
    nn.Conv2d(256, 512, 4, 2, 1, bias=False),
    nn.BatchNorm2d(512),
    nn.LeakyReLU(0.2, inplace=True),
    nn.Conv2d(512, 1, 4, 1, 0, bias=False),
    nn.Sigmoid(),
    nn.Flatten()
).to(device)                                        # ④

① 图像通过二维卷积层,输入通道数为3,输出通道数为64,卷积核大小4,步长2,填充1,且无偏置。
② 对第一层卷积的输出应用LeakyReLU激活函数。
③ 对第二层卷积的输出执行二维批量归一化。
④ 输出为一个介于0到1之间的单值,表示该图像为真实图像的概率。

判别器网络的输入是一张具有三个颜色通道的彩色图像。第一个二维卷积层是Conv2d(3, 64, 4, 2, 1, bias=False),表示输入为3个通道,输出64个通道,卷积核大小为4,步长为2,填充为1。网络中的每个二维卷积层都会对图像应用滤波器,提取空间特征。

从第二个二维卷积层开始,我们会对输出应用二维批量归一化(详见上一节讲解)和LeakyReLU激活函数(稍后会解释)。LeakyReLU是ReLU的改进版,允许输出在小于零的值上具有一个斜率。具体定义如下:

image.png

其中,β 是一个介于 0 和 1 之间的常数。LeakyReLU 激活函数常用于解决稀疏梯度问题(即大多数梯度变为零或接近零)。训练 DCGAN 就是一个典型的例子。当神经元的输入为负时,ReLU 的输出为零,导致神经元失活。而 LeakyReLU 会为负输入返回一个小的负值,而非零,这有助于保持神经元的活跃状态和学习能力,维持更好的梯度流,从而加快模型参数的收敛速度。

在构建服装生成的生成器时,我们将采用相同的思路。生成器的层设计将镜像 DCGAN 判别器中使用的层,示例如下。

代码示例 4.5 DCGAN 中生成器的设计

G=nn.Sequential(
    nn.ConvTranspose2d(100, 512, 4, 1, 0, bias=False),    # ①
    nn.BatchNorm2d(512),
    nn.ReLU(inplace=True),
    nn.ConvTranspose2d(512, 256, 4, 2, 1, bias=False),    # ②
    nn.BatchNorm2d(256),
    nn.ReLU(inplace=True),
    nn.ConvTranspose2d(256, 128, 4, 2, 1, bias=False),
    nn.BatchNorm2d(128),
    nn.ReLU(inplace=True),
    nn.ConvTranspose2d(128, 64, 4, 2, 1, bias=False),
    nn.BatchNorm2d(64),
    nn.ReLU(inplace=True),
    nn.ConvTranspose2d(64, 3, 4, 2, 1, bias=False),       # ③
    nn.Tanh()
).to(device)                                              # ④

① 生成器中的第一层设计对应判别器中的最后一层。
② 生成器中的第二层对应判别器中的倒数第二层(输入和输出通道数位置互换)。
③ 生成器中的最后一层对应判别器中的第一层。
④ 使用 Tanh() 激活函数,将输出层的数值压缩到 [–1, 1] 范围内,因为训练集中图像的数值范围是 [–1, 1]。

如图 4.8 所示,生成器通过五个二维转置卷积层生成图像,它们与判别器中的五个二维卷积层呈对称关系。例如,生成器的最后一层 ConvTranspose2d(64, 3, 4, 2, 1, bias=False) 是基于判别器的第一层 Conv2d(3, 64, 4, 2, 1, bias=False) 设计的。判别器中 Conv2d 的输入和输出通道数在生成器中的 ConvTranspose2d 中互换,作为输出和输入通道数使用。

image.png

图 4.8 设计 DCGAN 中的生成器网络,通过镜像判别器网络中的层来生成动漫人脸。图的右侧展示了判别器网络,该网络包含五个二维卷积层。为了设计一个能够凭空生成动漫人脸的生成器,我们将判别器网络中的层进行镜像,如图左侧所示,生成器拥有五个二维转置卷积层,它们与判别器中的二维卷积层对称。此外,在前四层中,判别器的输入通道数和输出通道数互换,分别作为生成器的输出通道数和输入通道数使用。

生成器第一层二维转置卷积层的输入通道数为 100,这是因为生成器从潜在空间(图 4.8 左下角)获取了一个长度为 100 的随机噪声向量,并将其输入到生成器中。生成器最后一层二维转置卷积层的输出通道数为 3,因为输出是具有三个颜色通道(RGB)的图像。我们对生成器的输出应用 Tanh 激活函数,将所有数值压缩到 [–1, 1] 范围内,因为训练图像的数值均在该范围内。

如同以往,损失函数采用二元交叉熵损失(binary cross-entropy loss)。判别器试图最大化二分类的准确率:正确识别真实样本为真实,假样本为假。生成器则试图最小化假样本被判别为假的概率。

我们为判别器和生成器都采用 Adam 优化器,并将学习率设置为 0.0002:

loss_fn = nn.BCELoss()
lr = 0.0002
optimG = torch.optim.Adam(G.parameters(), lr=lr, betas=(0.5, 0.999))
optimD = torch.optim.Adam(D.parameters(), lr=lr, betas=(0.5, 0.999))

你在第2章见过 Adam 优化器,但当时使用的是默认的 betas 参数。在这里,我们选择了不同于默认值的 betas。Adam 优化器中的 betas 参数对训练过程的稳定性和加速收敛起着关键作用。它们通过控制对近期梯度信息(beta1)与过去梯度信息的权重,以及基于梯度信息确定性自适应调整学习率(beta2)来实现这一点。这些参数通常需要根据具体问题的特性进行微调。

4.5.2 训练和使用 DCGAN

DCGAN 的训练过程与我们之前训练其他 GAN 模型(如第3章以及本章早些时候的模型)类似。由于我们并不知道动漫人脸图像的真实分布,因此我们将依赖可视化技术来判断训练是否完成。具体来说,我们定义了一个 test_epoch() 函数,用于在每个训练周期后可视化生成器生成的动漫人脸图像:

def test_epoch():
    noise = torch.randn(32, 100, 1, 1).to(device=device)      ①
    fake_samples = G(noise).cpu().detach()                    ②
    for i in range(32):                                        ③
        ax = plt.subplot(4, 8, i + 1)
        img = (fake_samples.cpu().detach()[i] / 2 + 0.5).permute(1, 2, 0)
        plt.imshow(img)
        plt.xticks([])
        plt.yticks([])
    plt.subplots_adjust(hspace=-0.6)
    plt.show()

test_epoch()                                                 ④

① 从潜在空间获取 32 个随机噪声向量
② 生成 32 张动漫人脸图像
③ 以 4 × 8 的网格排列显示生成的图像
④ 调用函数,在训练模型前生成图像

如果你运行上述代码单元,你会看到 32 张看起来像电视机上雪花噪点的图像,因为我们还没有训练生成器,这些图像根本不像动漫人脸。

我们定义了三个函数:train_D_on_real()train_D_on_fake()train_G(),它们与本章前面用于训练生成灰度服装图像 GAN 的函数类似。请参考本章 GitHub 仓库中的 Jupyter Notebook 熟悉这些函数。它们依次用真实图像训练判别器,用假图像训练判别器,最后训练生成器。

接下来,我们训练模型 20 个周期:

for i in range(20):
    gloss = 0
    dloss = 0
    for n, (real_samples, _) in enumerate(train_loader):
        loss_D = train_D_on_real(real_samples)
        dloss += loss_D
        loss_D = train_D_on_fake()
        dloss += loss_D
        loss_G = train_G()
        gloss += loss_G
    gloss = gloss / n
    dloss = dloss / n
    print(f"epoch {i+1}, dloss: {dloss}, gloss {gloss}")
    test_epoch()

如果你使用 GPU 训练,整个训练过程大约需要 20 分钟。否则,训练时间可能会根据你电脑的硬件配置,延长到 2 到 3 小时。你也可以从我的网站下载已训练好的模型:gattonweb.uky.edu/faculty/liu…

每完成一个训练周期后,你都可以可视化生成的动漫人脸图像。仅训练一个周期后,模型就能生成看起来像动漫人脸的彩色图像,如图 4.9 所示。随着训练的进行,生成图像的质量会越来越好。

image.png

我们将丢弃判别器,并将训练好的生成器保存到本地文件夹中:

scripted = torch.jit.script(G)
scripted.save('files/anime_gen.pt')

要使用训练好的生成器,我们加载模型并用它生成32张图像:

new_G = torch.jit.load('files/anime_gen.pt', map_location=device)
new_G.eval()
noise = torch.randn(32, 100, 1, 1).to(device)
fake_samples = new_G(noise).cpu().detach()
for i in range(32):
    ax = plt.subplot(4, 8, i + 1)
    img = (fake_samples.cpu().detach()[i] / 2 + 0.5).permute(1, 2, 0)
    plt.imshow(img)
    plt.xticks([])
    plt.yticks([])
plt.subplots_adjust(hspace=-0.6)
plt.show()

生成的动漫头像如图4.10所示。生成的图像与图4.7中训练集中的图像非常相似。

image.png

图4.10 由训练好的DCGAN生成器生成的动漫头像图像

你可能注意到生成图像的发色各异:有黑色、红色,还有金色。你可能会想:能否让生成器生成带有特定特征的图像,比如黑发或红发?答案是可以的。在第5章中,你将学习几种在GAN生成图像中选择特征的方法。

总结

  • 为了凭空生成逼真的图像,生成器会镜像判别器网络中使用的层。

  • 虽然仅使用全连接层可以生成灰度图像,但生成高分辨率彩色图像时,我们需要使用卷积神经网络(CNN)。

  • 二维卷积层用于特征提取。它们对输入数据应用一组可学习的滤波器(也称为核),以在不同的空间尺度上检测模式和特征。这些层对于捕捉输入数据的层级表示至关重要。

  • 二维转置卷积层(也称为反卷积或上采样层)用于上采样或生成高分辨率特征图。它们对输入数据应用滤波器,但与标准卷积不同,它们通过在输出值之间插入间隙来增加空间维度,从而有效地“放大”特征图。这个过程产生更高分辨率的特征图。

  • 二维批量归一化是一种深度学习和神经网络中常用的技术,用于改善CNN及其他处理二维数据(如图像)模型的训练和性能。它对每个特征通道的值进行归一化,使其均值为0,标准差为1,有助于稳定并加速训练过程。