深度学习365天训练营打卡:第G2周:人脸图像生成(DCGAN)

第G2周:人脸图像生成(DCGAN)

🍨 本文為🔗365天深度學習訓練營 中的學習紀錄博客
🍖 原作者:K同学啊 | 接輔導、項目定制

基础任务

    1. 了解什么是深度卷积生成对抗网络
    1. 了解DCGAN与GAN的区别与联系
    1. 学习本文代码,并跑通代码

进阶任务

  • 调用训练好的模型生成新图像

一、理论基础

DCGAN 原理

深度卷积对抗网络(Deep Convolutional Generative Adversarial Networks, DCGAN)是生成对抗网络的一种模型改进,其将卷积运算的思想引入到生成式模型当中来做无监督的训练,利用卷积网络强大的特征提取能力来提高生成网络的学习效果。

DCGAN 模型具有以下特点:

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

其与GAN的区别是,它继承了GAN的思想,但是图像生成和判别中的维度转换均借助CNN实现,不涉及全连接的维度转换。

二、项目介绍

🏡 我的环境

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

1.基本流程

首先读取minst数据集,并将其标准化以及其他的数据预处理。然后定义生成器和判别器,生成器其功能是从一个分布中生成噪声,然后再用该噪声生成图像。我认为这个噪声本质上是一个随机抽样,生成噪声的分布中的每一个点都有一个对应的图像生成规则,从而能够通过生成器去生成图像。判别器就比较容易理解,只是一个判断图像是否为生成的图像。

2.代码介绍

设置超参数
import torch, 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
dataroot = r"/mnt/新建文件夹 (2)/图片"  # 数据路径
batch_size = 32  # 训练过程中批次大小
image_size = 64  # 图像的尺寸(宽度和高度)
nz = 100  # 潜在向量的大小(生成器输入的尺寸)
ngf = 64  # 生成器中的特征图大小
ndf = 64  # 判别器中的特征图大小
num_epochs = 500  # 训练的总轮数,如果你显卡不太行,可调小,但是生成效果会随之降低
lr = 0.0002  # 学习率
beta1 = 0.5  # Adam优化器的Beta1超参数
读取数据集
# 我们可以按照我们设置的方式使用图像文件夹数据集。

# 创建数据集
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)))

定义网络架构
# 自定义权重初始化函数,作用于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,标准差为0.02
        nn.init.normal_(m.weight.data, 1.0, 0.02)
        # 使用常数初始化偏置项数据,值为0
        nn.init.constant_(m.bias.data, 0)   
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)     
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)
# 创建判别器模型
netD = Discriminator().to(device)

# 应用 "weights_init" 函数来随机初始化所有权重
# 使用 mean=0, stdev=0.2 的方式进行初始化
netD.apply(weights_init)

# 打印模型
print(netD)       
模型训练
# 初始化“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))
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

在这里插入图片描述

结果生成
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()

在这里插入图片描述

# 创建一个大小为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())

在这里插入图片描述

# 从数据加载器中获取一批真实图像
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()

在这里插入图片描述

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值