PyTorch实战(8)——深度卷积生成对抗网络
PyTorch实战(8)——深度卷积生成对抗网络
- 0. 前言
- 1. 卷积与反卷积
- 1.1 卷积
- 1.2 反卷积
- 2. 批归一化
- 3. 数据处理
- 3.1 数据集介绍
- 3.2 PyTorch 中的通道优先
- 4. 深度卷积生成对抗网络
- 4.1 模型构建
- 4.2 模型训练
- 小结
- 系列链接
0. 前言
在本节中,我们的目标是创建高分辨率的彩色动漫面部图像,生成器通过镜像鉴别器网络中的步骤来生成图像。但本节中所使用高分辨率彩色图像包含的像素比低分辨率灰度图像多得多。如果仅使用全连接层,模型中的参数数量会大幅增加,导致训练效率低下。因此,我们转向使用卷积神经网络 (Convolutional Neural Network, CNN)。在 CNN
中,每个神经元仅与输入的一个小区域相连接,这种局部连接减少了参数数量,使得网络更加高效,从而加快了训练时间并降低了计算成本。CNN
能够更有效地捕捉图像数据中的空间层次结构,因为它们将图像视为多维对象,而不是一维向量。我们将使用卷积对输入图像进行下采样并提取其空间特征,鉴别器网络使用卷积层,而生成器通过使用转置卷积层(也称为反卷积或上采样层)来镜像卷积层进行上采样,以生成高分辨率的特征图。
1. 卷积与反卷积
卷积层和反卷积层是卷积神经网络中的两个基本构建块,广泛应用于图像处理和计算机视觉任务。它们有不同的目的和特性:卷积层用于特征提取,将一组带有可学习参数的的卷积核应用于输入数据,以检测不同空间尺度上的模式和特征,卷积层对于捕获输入数据的层次化表示至关重要;而反卷积层用于上采样或生成高分辨率的特征图。
1.1 卷积
为了创建高分辨率的彩色图像,我们需要比简单的全连接神经网络更复杂的技术。具体来说,我们将使用卷积神经网络 (Convolutional Neural Network
, CNN
),它们特别适合处理具有网格状拓扑的数据,如图像。卷积神经网络与全连接网络有所不同。首先,在卷积神经网络中,每个神经元只与输入的一个小区域相连接。这是基于在图像数据中,局部像素组之间的相关性可能更高。这种局部连接减少了参数的数量,从而使网络更高效。其次,卷积神经网络使用共享权重的概念——相同的权重在输入的不同区域之间共享。这类似于在整个输入空间上滑动一个卷积核,这个卷积核器可以检测特定的特征(例如边缘或纹理),而无论它们在输入中的位置如何,从而实现平移不变性。
基于上述原因,卷积神经网络在图像处理上更为高效。与类似规模的全连接网络相比,它们所需的参数更少,从而导致训练时间更短、计算成本更低,通常在捕捉图像数据的空间层次结构方面也更为有效。有关卷积的详细介绍,参考《卷积神经网络 (Convolutional Neural Network, CNN)》。
1.2 反卷积
反卷积也称转置卷积,与卷积层相反,转置卷积层通过上采样和填充图像中的空隙来生成特征,并利用卷积核提高图像分辨率。在转置卷积层中,输出尺寸通常比输入更大。因此,转置卷积层是生成高分辨率图像时是必不可少的工具。为了更清楚地理解反卷积操作的工作原理,我们使用如下简单示例,假设有一个非常小的 2 × 2
输入图像。
输入图像中的值如下:
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]) # 使用自定义权重和偏置替换转置卷积层中的权重和偏置
以上转置卷积层有一个输入通道、一个输出通道,卷积核大小为 2 × 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
的矩阵。首先,根据步长 stride=2
,在输入元素之间插入 stride-1=1
个零,然后将扩张后的输入与转置后的卷积核进行普通卷积,通过滑动窗口计算每个位置的输出值。
2. 批归一化
批归一化 (Batch Normalization
) 是现代深度学习框架中的一种正则化技术,已成为有效训练深度神经网络的关键组成部分。
在批归一化中,归一化是针对每个特征通道独立进行的,通过调整和缩放通道中的值,使其均值为 0
,方差为 1
。特征通道是指卷积神经网络中多维张量的一个维度,用于表示输入数据的不同方面或特征。例如,它们可以代表颜色通道,如红色、绿色或蓝色。归一化确保了网络中较深层的输入分布在训练过程中保持更加稳定。这种稳定性来源于归一化过程减少了内部协方差偏移 (internal covariate shift
),即由于下层权重更新而导致的网络激活分布变化。它还有助于解决梯度消失或爆炸的问题,通过保持输入在合适的范围内,防止梯度变得过小(消失)或过大(爆炸)。
批归一化的工作原理如下:对于每个特征通道,首先计算该通道内所有观测值的均值和方差。然后,使用之前得到的均值和方差对每个特征通道的值进行归一化(通过从每个观测值中减去均值,然后除以标准差)。这确保了每个通道中的值经过归一化后均值为 0
,标准差为 1
,有助于稳定和加速训练,还有助于在反向传播过程中保持稳定的梯度,从而进一步促进深度神经网络的训练。
接下来,通过具体示例展示批归一化的工作原理。假设有一个大小为 64 × 64
的三通道输入,将该输入通过一个具有三个输出通道的卷积层处理,处理后的输出有三个通道,大小为 64 × 64
像素,如下所示:
# 创建一个 3 通道输入
img = torch.rand(1,3,64,64)
# 创建一个 2D 卷积层
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])
查看每个输出通道中像素的均值和标准差:
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())
输出结果如下所示,可以看到,每个输出通道中像素的平均值不是 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())
输出结果如下所示,可以看到,每个输出通道中像素的平均值几乎为 0
(或者是一个非常接近 0
的小数),每个输出通道中像素的标准差接近 1
,这就是批归一化的作用,对每个特征通道的观测值进行归一化,使得每个特征通道中的值具有 0
均值和单位标准差:
3. 数据处理
接下来,将学习如何生成高分辨率的彩色图像,训练数据是动漫面部的彩色图像。
3.1 数据集介绍
从 Kaggle
上下载训练数据,该数据集包含 63632
张动漫面部的彩色图像。下载完成后解压缩数据文件,将 zip
文件中的所有内容放在 files/anime/
目录下,即所有的动漫面部图像都在 /files/anime/images/
目录下。
(1) 定义路径名称,以便在 Pytorch
中加载图像:
anime_path = r"files/anime"
可以根据保存图像的位置更改路径的名称。需要注意的是,ImageFolder()
类使用图像所在目录的名称来识别图像所属的类别。因此,定义的 anime_path
中并不包括 images/
目录。
(2) 使用 Torchvision
中的 ImageFolder()
类加载数据集:
from torchvision import transforms as T
from torchvision.datasets import ImageFoldertransform = T.Compose([T.Resize((64, 64)), # 将图像大小更改为 64 × 64T.ToTensor(), # 将图像转换为 PyTorch 张量T.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5])]) # 将图像值归一化到 [-1, 1],涵盖三个颜色通道
# 加载数据
train_data = ImageFolder(root=anime_path,transform=transform)
(3) 将训练数据分成多个批次,每个批数据的大小为 128
:
from torch.utils.data import DataLoaderbatch_size = 128
train_loader = DataLoader(dataset=train_data, batch_size=batch_size, shuffle=True)
3.2 PyTorch 中的通道优先
PyTorch
采用通道优先 (channels-first
) 的方式处理彩色图像,这意味着在 PyTorch
中,图像的形状是 (number_channels, height, width)
。而在其他 Python
库中,如 TensorFlow
或 Matplotlib
,则采用通道后置 (channels-last
) 的方式,彩色图像的形状为 (height, width, number_channels)
。
(1) 查看数据集中的示例图像,并打印出其形状:
image0, _ = train_data[0]
print(image0.shape)
输出结果如下所示,可以看到,图像的形状为 3 × 64 × 64
,这意味着图像有三个颜色通道 (RGB
),图像的高度和宽度都是 64
像素:
torch.Size([3, 64, 64])
在 Matplotlib
中绘制图像时,需要使用 PyTorch
的 permute()
方法将其转换为通道后置格式:
import matplotlib.pyplot as pltplt.imshow(image0.permute(1,2,0)*0.5+0.5)
plt.show()
需要注意的是,需要将表示图像的 PyTorch
张量乘以 0.5
,然后加上 0.5
,将值从 [-1, 1]
范围转换到 [0, 1]
范围,以便进行可视化。
(2) 接下来,定义 plot_images()
函数,用于可视化图像:
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)
4. 深度卷积生成对抗网络
在本节中,我们将创建并训练一个深度卷积生成对抗网络 (Deep Convolution Generative Adversarial Network
, DCGAN
) 模型,生成动漫面部图像,模型由一个鉴别器网络和一个生成器网络组成,在网络中使用卷积层、转置卷积层和批归一化层。
首先,从鉴别器网络开始。然后,生成器网络通过镜像鉴别器网络架构进行构建,生成逼真的彩色图像。接下来,使用准备好的数据来训练模型,并使用训练好的模型生成新的动漫面部图像。
4.1 模型构建
(1) 鉴别器是一个二分类器,用于将样本分为真实或虚假,使用卷积层和批归一化层。高分辨率彩色图像包含大量像素,如果仅使用全连接层,很难有效训练模型:
import torch.nn as nn
import torchdevice = "cuda" if torch.cuda.is_available() else "cpu"
D = nn.Sequential(nn.Conv2d(3, 64, 4, 2, 1, bias=False), # 将图像通过 2D 卷积层nn.LeakyReLU(0.2, inplace=True), # 对第一层卷积的输出应用 LeakyReLU 激活函数nn.Conv2d(64, 128, 4, 2, 1, bias=False),nn.BatchNorm2d(128), # 对第二层卷积的输出执行 2D 批归一化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(), # 输出是一个介于 0 和 1 之间的值,可以定义为图像为真实图像的概率nn.Flatten()).to(device)
鉴别器网络的输入是具有三个颜色通道的彩色图像。第一个卷积层是 Conv2d(3, 64, 4, 2, 1, bias=False)
,这意味着输入图像有 3
个通道,输出特征图有 64
个通道,卷积核大小为 4
,步幅为 2
,填充为 1
。网络中的每个卷积层都会对图像应用卷积核,以提取空间特征。
从第二个卷积层开始,对输出应用批归一化和 LeakyReLU
激活函数。LeakyReLU
激活函数是 ReLU
的一种改进版本,定义如下:
f ( a , x ) = { − β x x ≤ 0 x x > 0 f(a,x)= \begin{cases} -\beta x& {x≤0}\\ x& {x>0} \end{cases} f(a,x)={−βxxx≤0x>0
其中, β β β 是一个介于 0
和 1
之间的常数。LeakyReLU
激活函数通常用于解决稀疏梯度问题(即大多数梯度变为零或接近零的情况)。当神经元的输入为负时,ReLU
的输出为零,这样神经元变得不活跃。而 LeakyReLU
对负输入返回一个小的负值,而不是零,这有助于保持神经元活跃并继续学习,从而保持更好的梯度流动,并加速模型参数的收敛。
(2) 镜像鉴别器的模型架构构建生成器:
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 之间
如下图所示,生成器使用五个转置卷积层来创建图像,它们与鉴别器中的五个卷积层对称。例如,最后一层 ConvTranspose2d(64, 3, 4, 2, 1, bias=False)
是根据鉴别器中的第一层 Conv2d(3, 64, 4, 2, 1, bias=False)
构建的。交换 Conv2d
中输入和输出通道数,在 ConvTranspose2d
中分别作为输出和输入通道数。
第一层转置卷积层的输入通道数是 100
,因为生成器从潜空间获取一个 100
维的随机噪声向量,并将其输入到生成器中。生成器最后一层 2D
转置卷积层的输出通道数是 3
,因为输出是一个具有三个颜色通道的图像。对生成器的输出应用 Tanh
激活函数,将所有值压缩到 [-1, 1]
的范围内,因为训练图像的像素值都在 [-1, 1]
之间。
损失函数使用二元交叉熵损失。鉴别器试图最大化二元分类的准确率,将真实样本识别为真实,虚假样本识别为虚假,生成器则试图最小化虚假样本被识别为虚假的概率。
(3) 使用 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))
Adam
优化器中的 beta
参数在稳定并加速训练过程的收敛中起着至关重要的作用,通过控制近期与过去的梯度信息之间的权重 (beta1
) 以及根据梯度信息的不确定性 (beta2
) 调整学习率实现。
4.2 模型训练
(1) 由于我们不知道动漫面孔图像的真实分布,将依赖于可视化技术来判断训练是否完成。具体而言,定义一个 test_epoch()
函数,用于在每个训练轮次结束后可视化生成器创建的动漫面孔:
def test_epoch():noise=torch.randn(32,100,1,1).to(device=device) # 从潜空间获取 32 个随机噪声向量fake_samples=G(noise).cpu().detach() # 生成 32 张动漫人脸图像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()
运行代码,可以看到生成的图像如下所示,它们完全不像动漫面孔,因为生成器还未充分经过训练。
(2) 定义三个函数:train_D_on_real()
、train_D_on_fake()
和 train_G()
,先使用真实图像训练鉴别器,再使用虚假图像训练鉴别器,最后训练生成器:
real_labels=torch.ones((batch_size,1)).to(device)
fake_labels=torch.zeros((batch_size,1)).to(device)def train_D_on_real(real_samples):real_samples=real_samples.to(device)preds=D(real_samples)labels=torch.ones((real_samples.shape[0],1)).to(device)loss_D=loss_fn(preds,labels)optimD.zero_grad()loss_D.backward()optimD.step()return loss_D def train_D_on_fake():noise=torch.randn(batch_size,100,1,1).to(device)generated_data=G(noise)preds=D(generated_data)loss_D=loss_fn(preds,fake_labels)optimD.zero_grad()loss_D.backward()optimD.step()return loss_D def train_G():noise=torch.randn(batch_size,100,1,1).to(device)generated_data=G(noise)preds=D(generated_data)loss_G=loss_fn(preds,real_labels)optimG.zero_grad()loss_G.backward()optimG.step()return loss_G
(3) 接下来,将模型训练 20
个 epoch
:
for i in range(20):gloss=0dloss=0for n, (real_samples,_) in enumerate(train_loader): loss_D=train_D_on_real(real_samples)dloss+=loss_Dloss_D=train_D_on_fake()dloss+=loss_Dloss_G=train_G()gloss+=loss_Ggloss=gloss/ndloss=dloss/nprint(f"epoch {i+1}, dloss: {dloss}, gloss {gloss}")test_epoch()
在每个训练 epoch
后,可以可视化生成的动漫面孔,随着训练的进行,生成图像的质量会越来越好。
(4) 丢弃鉴别器,并将训练好的生成器保存在本地文件夹中:
scripted = torch.jit.script(G)
scripted.save('files/anime_gen.pt')
(5) 使用训练好的生成器,加载模型并用它生成 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()
生成的动漫面孔如下图所示,生成的图像与训练集中的图像非常相似。
小结
为了生成高分辨率的彩色图像,需要使用卷积神经网络。卷积层用于特征提取。对输入数据应用一组可学习的卷积核,以在不同的空间尺度上检测模式和特征,这对于捕捉输入数据的层次表示至关重要。转置卷积层(也称为反卷积或上采样层)用于上采样或生成高分辨率特征图。对输入数据应用卷积核,但与标准卷积不同,通过扩展输入来增加空间维度,从而有效地放大特征图,生成了更高分辨率的特征图。
批归一化是深度学习和神经网络中常用的技术,旨在提高模型的训练和性能。批归一化对每个特征通道的值进行归一化,使其均值为 0
,标准差为 1
,这有助于稳定和加速训练过程。
系列链接
PyTorch生成式人工智能实战:从零打造创意引擎
PyTorch实战(1)——神经网络与模型训练过程详解
PyTorch实战(2)——PyTorch基础
PyTorch实战(3)——使用PyTorch构建神经网络
PyTorch实战(4)——卷积神经网络详解
PyTorch实战(5)——分类任务详解
PyTorch实战(6)——生成模型(Generative Model)详解
PyTorch实战(7)——生成对抗网络实践详解