• 本文为365天深度学习练习营 中的学习记录博客
  • 原作者:K同学啊
  • 参阅文章:GAN入门|第二篇:人脸图画生成(DCGAN)

我的环境

  • 言语环境:Python3.10.11
  • 编译器:Jupyter Notebook
  • 深度学习框架:Pytorch 2.0.1+cu118
  • 显卡(GPU):NVIDIA GeForce RTX 4070

相关教程

  • 编译器教程:【新手入门深度学习 | 1-2:编译器Jupyter Notebook】
  • 深度学习环境配置教程:【新手入门深度学习 | 1-1:配置深度学习环境】
  • 一个深度学习小白需要的一切资料我都放这儿了:【新手入门深度学习 | 目录】

建议你学习本文之前先看看下面这篇入门文章,以便你可以更好的理解本文: 新手入门深度学习 | 2-1:图画数据建模流程示例

强烈建议咱们运用Jupyter Notebook编译器翻开源码,你接下来的操作将会十分便捷的!

  • 假如你是一名深度学习小白可以先看看我这个专门为你写的专栏: 《新手入门深度学习》
  • 假如你有必定基础,但是缺乏实战经验,可经过 《深度学习100例》 补齐基础
  • 另外,咱们正在经过 365天深度学习练习营 抱团学习,营内为咱们提供体系的学习教案专业的辅导十分杰出的学习氛围,欢迎你的加入

一、理论基础

1. DCGAN原理

深度卷积对立网络(Deep Convolutional Generative Adversarial Networks,简称DCGAN)是一种深度学习模型,由生成器(Generator)和判别器(Discriminator)两个神经网络组成。DCGAN结合了卷积神经网络(Convolutional Neural Networks,简称CNN)和生成对立网络(Generative Adversarial Networks,简称GAN)的思维,用于生成传神的图画。

其是生成对立网络的一种模型改善,其将卷积运算的思维引进到生成式模型当中来做无监督的练习,运用卷积网络强大的特征提取才能来进步生成网络的学习效果。DCGAN模型有以下特点:

    1. 判别器模型运用卷积步长替代了空间池化,生成器模型中运用反卷积操作扩展数据维度
    1. 除了生成器模型的输出层和判别器模型的输入层,在整个对立网络的其它层上都运用了Batch Normalization,原因是Batch Normalization可以安稳学习,有助于优化初始化参数值不良而导致的练习问题。
    1. 整个网络去除了全连接层,直接运用卷积层连接生成器和判别器的输入层以及输出层
    1. 生成器的输出层运用Tanh激活函数以控制输出范围,而在其它层中均运用了ReLU激活函数;在判别器上运用Leaky ReLU激活函数

GAN入门|第二篇:人脸图像生成(DCGAN)

图1所示了一种常见的 DCGAN 结构。首要包含了左面生成网络(Generator)和右边判别网络(Discriminator),其各有四个转置卷积层(DeConv)和四个卷积层(Conv)。其间4*4*512代表这一层共有512个巨细为4*4的特征图,括号内的BN和ReLU别离表明在卷积层之前和之后别离运用了批归一化和修正线性单元激活两种处理办法。Tanh 和 LeakyReLU 别离表明双切正切激活和弱修正线性激活。

2. 练习原理

如图2所示,DCGAN 模型首要包含了一个生成网络 G 和一个判别网络 D,生成网络 G 担任生成图画,它接受一个随机的噪声z,经过该噪声生成图画,将生成的图画记为G(z),判别网络D担任判别一张图画是否为实在的,它的输入是x,代表一张图画,输出D(x)表明x为实在图画的概率。 实际上判别网络D是对数据的来源进行一个判别:终究这个数据是来自实在的数据分布Pd(x)判别为“1”),仍是来自于一个生成网络G所发生的一个数据分布Pg(z)(判别为“0”)。所以在整个练习进程中,生成网络G的目标是生成可以以假乱真的图画G(z),当判别网络D无法区分,即D(G(z))=0.5时,便得到了一个生成网络G用来生成图画扩充数据集。

GAN入门|第二篇:人脸图像生成(DCGAN)

二、前期预备

我的环境

  • 言语环境:Python3.10.11
  • 编译器:Jupyter Lab
  • 深度学习环境:Pytorch
    • torch==1.12.1+cu113
    • torchvision==0.13.1+cu113
  • 显卡(GPU):NVIDIA GeForce RTX 4070
  • 数据集: GAN入门|第二篇:人脸图画生成(DCGAN)

1. 导入第三方库

import torch, random, random, os
import torch.nn as nn
import torch.nn.parallel
import torch.optim as optim
import torch.utils.data
import torchvision.datasets as dset
import torchvision.transforms as transforms
import torchvision.utils as vutils
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from IPython.display import HTML
manualSeed = 999  # 随机种子
print("Random Seed: ", manualSeed)
random.seed(manualSeed)
torch.manual_seed(manualSeed)
torch.use_deterministic_algorithms(True) # Needed for reproducible results
Random Seed:  999

2. 设置超参数

dataroot = "E:/Jupyter Lab/dataK/GAN-Data/"  # 数据路径
batch_size = 128  # 练习进程中的批次巨细
image_size = 64   # 图画的尺度(宽度和高度)
nz  = 100         # z潜在向量的巨细(生成器输入的尺度)
ngf = 64          # 生成器中的特征图巨细
ndf = 64          # 判别器中的特征图巨细
num_epochs = 5    # 练习的总轮数
lr    = 0.0002    # 学习率
beta1 = 0.5       # Adam优化器的Beta1超参数

3. 导入数据

这段代码的作用是预备用于练习的图画数据。它首先运用ImageFolder类创立一个数据集目标,该目标表明从文件夹中加载的图画数据集。然后,经过transforms.Compose组合了一系列图画改换操作来对图画进行预处理,包含调整巨细、中心裁剪、转换为张量以及标准化。接着,运用DataLoader类创立一个数据加载器目标,该目标可以在练习进程中按批次加载数据,并可以挑选是否打乱数据集以及运用多线程加载数据。代码还挑选设备(GPU或CPU)来运转代码,并打印所挑选的设备。终究,代码经过数据加载器获取一批练习图画,并运用Matplotlib库制作这些图画。

# 咱们可以依照咱们设置的方式运用图画文件夹数据集。
# 创立数据集
dataset = dset.ImageFolder(root=dataroot,
                           transform=transforms.Compose([
                           transforms.Resize(image_size),        # 调整图画巨细
                           transforms.CenterCrop(image_size),    # 中心裁剪图画
                           transforms.ToTensor(),                # 将图画转换为张量
                           transforms.Normalize((0.5, 0.5, 0.5), # 标准化图画张量
                                                (0.5, 0.5, 0.5)),
                           ]))
# 创立数据加载器
dataloader = torch.utils.data.DataLoader(dataset, 
                                         batch_size=batch_size,  # 批量巨细
                                         shuffle=True,           # 是否打乱数据集
                                         num_workers=5 # 运用多个线程加载数据的作业进程数
                                        )
# 挑选要在哪个设备上运转代码
device = torch.device("cuda:0" if (torch.cuda.is_available()) else "cpu")
print("运用的设备是:",device)
# 制作一些练习图画
real_batch = next(iter(dataloader))
plt.figure(figsize=(8,8))
plt.axis("off")
plt.title("Training Images")
plt.imshow(np.transpose(vutils.make_grid(real_batch[0].to(device)[:24], 
                                         padding=2, 
                                         normalize=True).cpu(),(1,2,0)))

运用的设备是: cuda:0

GAN入门|第二篇:人脸图像生成(DCGAN)

三、界说模型

GAN入门|第二篇:人脸图像生成(DCGAN)

1. 初始化权重

# 自界说权重初始化函数,作用于netG和netD
def weights_init(m):
    # 获取当时层的类名
    classname = m.__class__.__name__
    # 假如类名中包含'Conv',即当时层是卷积层
    if classname.find('Conv') != -1:
        # 运用正态分布初始化权重数据,均值为0,标准差为0.02
        nn.init.normal_(m.weight.data, 0.0, 0.02)
    # 假如类名中包含'BatchNorm',即当时层是批归一化层
    elif classname.find('BatchNorm') != -1:
        # 运用正态分布初始化权重数据,均值为1,标准差为0.02
        nn.init.normal_(m.weight.data, 1.0, 0.02)
        # 运用常数初始化偏置项数据,值为0
        nn.init.constant_(m.bias.data, 0)

2. 界说生成器

class Generator(nn.Module):
    def __init__(self):
        super(Generator, self).__init__()
        self.main = nn.Sequential(
            # 输入为Z,经过一个转置卷积层
            nn.ConvTranspose2d(nz, ngf * 8, 4, 1, 0, bias=False),
            nn.BatchNorm2d(ngf * 8),  # 批归一化层,用于加速收敛和安稳练习进程
            nn.ReLU(True),  # ReLU激活函数
            # 输出尺度:(ngf*8) x 4 x 4
            nn.ConvTranspose2d(ngf * 8, ngf * 4, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ngf * 4),
            nn.ReLU(True),
            # 输出尺度:(ngf*4) x 8 x 8
            nn.ConvTranspose2d(ngf * 4, ngf * 2, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ngf * 2),
            nn.ReLU(True),
            # 输出尺度:(ngf*2) x 16 x 16
            nn.ConvTranspose2d(ngf * 2, ngf, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ngf),
            nn.ReLU(True),
            # 输出尺度:(ngf) x 32 x 32
            nn.ConvTranspose2d(ngf, 3, 4, 2, 1, bias=False),
            nn.Tanh()  # Tanh激活函数
            # 输出尺度:3 x 64 x 64
        )
    def forward(self, input):
        return self.main(input)
# 创立生成器
netG = Generator().to(device)
# 运用 "weights_init" 函数对一切权重进行随机初始化,
# 平均值(mean)设置为0,标准差(stdev)设置为0.02。
netG.apply(weights_init)
# 打印生成器模型
print(netG)
Generator(
  (main): Sequential(
    (0): ConvTranspose2d(100, 512, kernel_size=(4, 4), stride=(1, 1), bias=False)
    (1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (2): ReLU(inplace=True)
    (3): ConvTranspose2d(512, 256, kernel_size=(4, 4), stride=(2, 2), padding=(1, 1), bias=False)
    (4): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (5): ReLU(inplace=True)
    (6): ConvTranspose2d(256, 128, kernel_size=(4, 4), stride=(2, 2), padding=(1, 1), bias=False)
    (7): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (8): ReLU(inplace=True)
    (9): ConvTranspose2d(128, 64, kernel_size=(4, 4), stride=(2, 2), padding=(1, 1), bias=False)
    (10): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (11): ReLU(inplace=True)
    (12): ConvTranspose2d(64, 3, kernel_size=(4, 4), stride=(2, 2), padding=(1, 1), bias=False)
    (13): Tanh()
  )
)

3. 界说鉴别器

class Discriminator(nn.Module):
    def __init__(self):
        super(Discriminator, self).__init__()
        # 界说判别器的首要结构,运用Sequential容器将多个层按顺序组合在一起
        self.main = nn.Sequential(
            # 输入巨细为3 x 64 x 64
            nn.Conv2d(3, ndf, 4, 2, 1, bias=False),
            nn.LeakyReLU(0.2, inplace=True),
            # 输出巨细为(ndf) x 32 x 32
            nn.Conv2d(ndf, ndf * 2, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ndf * 2),
            nn.LeakyReLU(0.2, inplace=True),
            # 输出巨细为(ndf*2) x 16 x 16
            nn.Conv2d(ndf * 2, ndf * 4, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ndf * 4),
            nn.LeakyReLU(0.2, inplace=True),
            # 输出巨细为(ndf*4) x 8 x 8
            nn.Conv2d(ndf * 4, ndf * 8, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ndf * 8),
            nn.LeakyReLU(0.2, inplace=True),
            # 输出巨细为(ndf*8) x 4 x 4
            nn.Conv2d(ndf * 8, 1, 4, 1, 0, bias=False),
            nn.Sigmoid()
        )
    def forward(self, input):
        # 将输入经过判别器的首要结构进行前向传达
        return self.main(input)

判别器的首要结构是一系列的卷积层、批标准化层、激活函数层的组合。经过这些层的组合,输入的图画逐渐被降维并提取特征,终究输出一个标量值,表明输入图画被判别为实在图画的概率。 在前向传达办法(forward)中,输入数据经过判别器的首要结构进行前向传达,并返回输出成果。

# 创立判别器模型
netD = Discriminator().to(device)
# 应用 "weights_init" 函数来随机初始化一切权重
# 运用 mean=0, stdev=0.2 的方式进行初始化
netD.apply(weights_init)
# 打印模型
print(netD)
Discriminator(
  (main): Sequential(
    (0): Conv2d(3, 64, kernel_size=(4, 4), stride=(2, 2), padding=(1, 1), bias=False)
    (1): LeakyReLU(negative_slope=0.2, inplace=True)
    (2): Conv2d(64, 128, kernel_size=(4, 4), stride=(2, 2), padding=(1, 1), bias=False)
    (3): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (4): LeakyReLU(negative_slope=0.2, inplace=True)
    (5): Conv2d(128, 256, kernel_size=(4, 4), stride=(2, 2), padding=(1, 1), bias=False)
    (6): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (7): LeakyReLU(negative_slope=0.2, inplace=True)
    (8): Conv2d(256, 512, kernel_size=(4, 4), stride=(2, 2), padding=(1, 1), bias=False)
    (9): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (10): LeakyReLU(negative_slope=0.2, inplace=True)
    (11): Conv2d(512, 1, kernel_size=(4, 4), stride=(1, 1), bias=False)
    (12): Sigmoid()
  )
)

四、练习模型

1. 界说练习参数

# 初始化“BCELoss”丢失函数
criterion = nn.BCELoss()
# 创立用于可视化生成器进程的潜在向量批次
fixed_noise = torch.randn(64, nz, 1, 1, device=device)
real_label = 1.
fake_label = 0.
# 为生成器(G)和判别器(D)设置Adam优化器
optimizerD = optim.Adam(netD.parameters(), lr=lr, betas=(beta1, 0.999))
optimizerG = optim.Adam(netG.parameters(), lr=lr, betas=(beta1, 0.999))

2. 练习模型

img_list = []  # 用于存储生成的图画列表
G_losses = []  # 用于存储生成器的丢失列表
D_losses = []  # 用于存储判别器的丢失列表
iters = 0  # 迭代次数
print("Starting Training Loop...")  # 输出练习开始的提示信息
# 关于每个epoch(练习周期)
for epoch in range(num_epochs):
    # 关于dataloader中的每个batch
    for i, data in enumerate(dataloader, 0):
        ############################
        # (1) 更新判别器网络:最大化 log(D(x)) + log(1 - D(G(z)))
        ###########################
        ## 运用实在图画样本练习
        netD.zero_grad()  # 铲除判别器网络的梯度
        # 预备实在图画的数据
        real_cpu = data[0].to(device)
        b_size = real_cpu.size(0)
        label = torch.full((b_size,), real_label, dtype=torch.float, device=device)  # 创立一个满是实在标签的张量
        # 将实在图画样本输入判别器,进行前向传达
        output = netD(real_cpu).view(-1)
        # 核算实在图画样本的丢失
        errD_real = criterion(output, label)
        # 经过反向传达核算判别器的梯度
        errD_real.backward()
        D_x = output.mean().item()  # 核算判别器对实在图画样本的输出的平均值
        ## 运用生成图画样本练习
        # 生成一批潜在向量
        noise = torch.randn(b_size, nz, 1, 1, device=device)
        # 运用生成器生成一批假图画样本
        fake = netG(noise)
        label.fill_(fake_label)  # 创立一个满是假标签的张量
        # 将一切生成的图画样本输入判别器,进行前向传达
        output = netD(fake.detach()).view(-1)
        # 核算判别器对生成图画样本的丢失
        errD_fake = criterion(output, label)
        # 经过反向传达核算判别器的梯度
        errD_fake.backward()
        D_G_z1 = output.mean().item()  # 核算判别器对生成图画样本的输出的平均值
        # 核算判别器的总丢失,包含实在图画样本和生成图画样本的丢失之和
        errD = errD_real + errD_fake
        # 更新判别器的参数
        optimizerD.step()
        ############################
        # (2) 更新生成器网络:最大化 log(D(G(z)))
        ###########################
        netG.zero_grad()  # 铲除生成器网络的梯度
        label.fill_(real_label)  # 关于生成器成本而言,将假标签视为实在标签
        # 因为刚刚更新了判别器,再次将一切生成的图画样本输入判别器,进行前向传达
        output = netD(fake).view(-1)
        # 依据判别器的输出核算生成器的丢失
        errG = criterion(output, label)
        # 经过反向传达核算生成器的梯度
        errG.backward()
        D_G_z2 = output.mean().item()  # 核算判别器对生成器输出的平均值
        # 更新生成器的参数
        optimizerG.step()
        # 输出练习统计信息
        if i % 400 == 0:
            print('[%d/%d][%d/%d]\tLoss_D: %.4f\tLoss_G: %.4f\tD(x): %.4f\tD(G(z)): %.4f / %.4f'
                  % (epoch, num_epochs, i, len(dataloader),
                     errD.item(), errG.item(), D_x, D_G_z1, D_G_z2))
        # 保存丢失值以便后续绘图
        G_losses.append(errG.item())
        D_losses.append(errD.item())
        # 经过保存生成器在固定噪声上的输出来查看生成器的功能
        if (iters % 500 == 0) or ((epoch == num_epochs-1) and (i == len(dataloader)-1)):
            with torch.no_grad():
                fake = netG(fixed_noise).detach().cpu()
            img_list.append(vutils.make_grid(fake, padding=2, normalize=True))
        iters += 1
Starting Training Loop...
[0/5][0/1619]	Loss_D: 1.9687	Loss_G: 6.0989	D(x): 0.6962	D(G(z)): 0.7207 / 0.0041
[0/5][400/1619]	Loss_D: 0.5378	Loss_G: 5.7189	D(x): 0.8812	D(G(z)): 0.2734 / 0.0072
[0/5][800/1619]	Loss_D: 0.2936	Loss_G: 3.3109	D(x): 0.8949	D(G(z)): 0.1119 / 0.0800
......
[4/5][400/1619]	Loss_D: 0.5831	Loss_G: 3.0852	D(x): 0.9077	D(G(z)): 0.3487 / 0.0612
[4/5][800/1619]	Loss_D: 1.1679	Loss_G: 3.6101	D(x): 0.8917	D(G(z)): 0.5836 / 0.0391
[4/5][1200/1619]	Loss_D: 0.6048	Loss_G: 3.1295	D(x): 0.8891	D(G(z)): 0.3597 / 0.0538
[4/5][1600/1619]	Loss_D: 0.5805	Loss_G: 3.3885	D(x): 0.8391	D(G(z)): 0.2936 / 0.0465

这段代码是一个典型的GAN练习循环。在练习进程中,首先更新判别器网络,然后更新生成器网络。在每个epoch的每个batch中,会进行以下操作:

  1. 更新判别器网络:经过练习实在图画样本和生成图画样本,最大化判别器的丢失。具体步骤如下:
  • 关于实在图画样本,核算判别器对实在图画样本的输出和实在标签之间的丢失,然后进行反向传达核算梯度。
  • 关于生成的图画样本,核算判别器对生成图画样本的输出和假标签之间的丢失,然后进行反向传达核算梯度。
  • 将实在图画样本的丢失和生成图画样本的丢失相加得到判别器的总丢失,并更新判别器的参数。
  1. 更新生成器网络:经过最大化生成器的丢失,迫使生成器发生更传神的图画样本。具体步骤如下:
  • 运用生成器生成一批假图画样本。
  • 将生成图画样本输入判别器,核算判别器对生成图画样本的输出和实在标签之间的丢失,并进行反向传达核算生成器的梯度。
  • 更新生成器的参数。
  1. 输出练习统计信息:每隔必定的步数,输出当时练习的epoch、batch以及判别器和生成器的丢失值等信息。
  2. 保存丢失值:将生成器和判别器的丢失值存储到相应的列表中,以便后续绘图和分析。
  3. 查看生成器的功能:每隔必定的步数或许在练习结束时,经过将固定的噪声输入生成器,生成一批图画样本,并保存到img_list列表中。这样可以观察生成器在练习进程中生成的图画质量的变化。
  4. 更新迭代次数:每完结一个batch的练习,将迭代次数iters加1。

总体来说,这段代码实现了GAN的练习进程,经过交替更新判别器和生成器的参数,目标是使生成器生成传神的图画样本,同时判别器可以准确区分实在图画样本和生成图画样本。

3. 可视化

plt.figure(figsize=(10,5))
plt.title("Generator and Discriminator Loss During Training")
plt.plot(G_losses,label="G")
plt.plot(D_losses,label="D")
plt.xlabel("iterations")
plt.ylabel("Loss")
plt.legend()
plt.show()

GAN入门|第二篇:人脸图像生成(DCGAN)

# 创立一个巨细为8x8的图形目标
fig = plt.figure(figsize=(8, 8))
# 不显现坐标轴
plt.axis("off")
# 将图画列表img_list中的图画转置并创立一个包含每个图画的单个列表ims
ims = [[plt.imshow(np.transpose(i, (1, 2, 0)), animated=True)] for i in img_list]
# 运用图形目标、图画列表ims以及其他参数创立一个动画目标ani
ani = animation.ArtistAnimation(fig, ims, interval=1000, repeat_delay=1000, blit=True)
# 将动画以HTML方式呈现
HTML(ani.to_jshtml())

GAN入门|第二篇:人脸图像生成(DCGAN)

# 从数据加载器中获取一批实在图画
real_batch = next(iter(dataloader))
# 制作实在图画
plt.figure(figsize=(15,15))
plt.subplot(1,2,1)
plt.axis("off")
plt.title("Real Images")
plt.imshow(np.transpose(vutils.make_grid(real_batch[0].to(device)[:64], padding=5, normalize=True).cpu(),(1,2,0)))
# 制作上一个时期生成的假图画
plt.subplot(1,2,2)
plt.axis("off")
plt.title("Fake Images")
plt.imshow(np.transpose(img_list[-1],(1,2,0)))
plt.show()

GAN入门|第二篇:人脸图像生成(DCGAN)