实战-DCGAN 生成人脸照片

664 阅读7分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第22天,点击查看活动详情

DCGAN 生成人脸照片

前言

数据集:Large-scale CelebFaces Attributes (CelebA) Dataset

CelebA是大规模面部属性数据集,包含20万张名人照片,每个图片有40个属性,数据集中的图像姿势和背景多样。

  • 10,177 个人
  • 202,599张脸
  • 每张图片有5个特征点坐标和40个二元特征

img

可以从官方的 Google Drive 进行下载即可。

生成器结构图如下:

image.png

判别器结构图如下:

image.png

文件说明

data目录存放原始数据与预处理后切分为训练集、验证机、测试集的数据
log目录训练过程中使用tensorboardX保存的指标数值,如损失、精确度等
model_save目录存放不同训练阶段的模型,最后找出个最优的用于测试集
config.py保存超参数
dataset_face.pyBanknote数据类,用于训练时获取数据
inference.py挑选模型在测试集上运行
generator.py生成器模型
discriminator.py判别器模型
trainer.py模型训练代码
data目录存放数据集
utils.py工具类文件

数据加载器

我们会用到 torchvisiontorchvision 是独立于 pytorch 的关于图像操作的一些方便工具库。torchvision 的详细介绍在:pypi.org/project/tor…

  • vision.datasets : 几个常用视觉数据集,可以下载和加载,这里主要的高级用法就是可以看源码如何自己写自己的Dataset的子类
  • vision.models : 流行的模型,例如 AlexNet, VGG, ResNet 和 Densenet 以及 与训练好的参数。
  • vision.transforms : 常用的图像操作,例如:随机切割,旋转,数据类型转换,图像到tensor ,numpy 数组到tensor , tensor 到 图像等。
  • vision.utils : 用于把形似 (3 x H x W) 的张量保存到硬盘中,给一个mini-batch的图像可以产生一个图像格网。

主要运用到 ImageFolder,是一个通用的数据加载器,图像应该按照以下方式放置:

root/dog/xxx.png
root/dog/xxy.png
root/dog/xxz.png

root/cat/123.png
root/cat/nsdf3.png
root/cat/asd932_.png
datasets.ImageFolder(root="root folder path", [transform, target_transform])

注:root 的地址应该是根目录,例如 root/

ImageFolder有以下成员:

  • self.classes - 类别名列表
  • self.class_to_idx - 类别名到标签,例如 “狗”-->[1,0,0]
  • self.imgs - 一个包括 (image path, class-index) 元组的列表。
# 读取图片文件
data_face = TD.ImageFolder(root=HP.data_root,
                           # 图片处理
                           transform=T.Compose([
                               T.Resize(HP.image_size), # 64x64x3 设置图片大小
                               T.CenterCrop(HP.image_size), # 剪裁图片,取中间
                               T.ToTensor(),    # to [0, 1] # 归一化
                               T.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))    # can't apply ImageNet statistic # 标准化
                           ]),
                           )

接下来设置数据加载器,配置相关超参数。

face_loader = DataLoader(data_face,
                         batch_size=HP.batch_size,
                         shuffle=True,
                         num_workers=HP.n_workers)

生成器

image.png

根据上诉图片进行配置:

首先第一个层是 projection_layer ,主要目的:shape 的转换,输出向量做一下压缩,从而能把高纬度的信息降维。

self.projection_layer = nn.Linear(HP.z_dim, 4*4*1024)

接下来是四个卷积,主要采用转置卷积(一种特殊的卷积,先padding来扩大图像尺寸,紧接着跟正向卷积一样,旋转卷积核180度,再进行卷积计算。),它的卷积核,步幅,填充相关参数配置主要实现 2 倍上采样。反卷积计算公式如下:

 out-size = stride ×( in-size 1)×+ kernel 2× padding \text { out-size } = \text { stride } \times(\text { in-size }-1) \times+\text { kernel }-2 \times \text { padding }
 self.generator = nn.Sequential(
 
            # TransposeConv layer: 1
            nn.ConvTranspose2d(in_channels=1024,    # [N, 512, 8, 8]
                               out_channels=512,
                               kernel_size=(4, 4),
                               stride=(2, 2),
                               padding=(1, 1),
                               bias=False),
            nn.BatchNorm2d(512),
            nn.ReLU(),
 
            # TransposeConv layer: 2
            nn.ConvTranspose2d(in_channels=512,  # [N, 256, 16, 16]
                               out_channels=256,
                               kernel_size=(4, 4),
                               stride=(2, 2),
                               padding=(1, 1),
                               bias=False),
            nn.BatchNorm2d(256),
            nn.ReLU(),
 
            # TransposeConv layer: 3
            nn.ConvTranspose2d(in_channels=256,  # [N, 128, 32, 32]
                               out_channels=128,
                               kernel_size=(4, 4),
                               stride=(2, 2),
                               padding=(1, 1),
                               bias=False),
            nn.BatchNorm2d(128),
            nn.ReLU(),
 
            # TransposeConv layer: final
            nn.ConvTranspose2d(in_channels=128,  # [N, 3, 64, 64]
                               out_channels=HP.data_channels,   # output channel: 3 (RGB)
                               kernel_size=(4, 4),
                               stride=(2, 2),
                               padding=(1, 1),
                               bias=False),
 
            nn.Tanh()  # [0, 1] Relu [0, inf]
        )

开始写前向传播函数,输入形状是 [N,100],第一层 projection_layer 的形状是 [N, 4*4*1024],接下来是卷积层输入,所以需要对第一层输出进行处理,改成 NCHW 这样的形式,也就是 [N, 1024, 4, 4]

def forward(self, latent_z):    # latent space (Ramdon Input / Noise) : [N, 100]
        z = self.projection_layer(latent_z) # [N, 4*4*1024]
        z_projected = z.view(-1, 1024, 4, 4) # [N, 1024, 4, 4]: NCHW
        return self.generator(z_projected)

对卷积层和 BatchNorm 进行参数初始化,方便模型进行约束。GAN 模型对参数比较敏感。

 	@staticmethod
    def weights_init(layer):
        layer_class_name = layer.__class__.__name__
        if 'Conv' in layer_class_name:
            nn.init.normal_(layer.weight.data, 0.0, 0.02)
        elif 'BatchNorm' in layer_class_name:
            nn.init.normal_(layer.weight.data, 1.0, 0.02)
            nn.init.normal_(layer.bias.data, 0.)

判别器

image.png 前面是 4 层卷积,它的卷积核,步幅,填充相关参数配置主要实现形状大小减半,后面接着使用了 BatchNorm,使用 LeakyReLU 作为激活函数。

self.discriminator = nn.Sequential( # 1. shape transform 2. use conv layer as "feature extraction"
            # conv layer : 1
            nn.Conv2d(in_channels=HP.data_channels, # [N, 64, 32, 32]
                      out_channels=64,
                      kernel_size=(3, 3),
                      stride=(2, 2),
                      padding=(1, 1),
                      bias=False),
            nn.LeakyReLU(0.2),
            # conv layer : 2
            nn.Conv2d(in_channels=64,  # [N, 128, 16, 16]
                      out_channels=128,
                      kernel_size=(3, 3),
                      stride=(2, 2),
                      padding=(1, 1),
                      bias=False),
            nn.BatchNorm2d(128),
            nn.LeakyReLU(0.2),
            # conv layer : 3
            nn.Conv2d(in_channels=128,  # [N, 256, 8, 8]
                      out_channels=256,
                      kernel_size=(3, 3),
                      stride=(2, 2),
                      padding=(1, 1),
                      bias=False),
            nn.BatchNorm2d(256),
            nn.LeakyReLU(0.2),
 
            # conv layer : 4
            nn.Conv2d(in_channels=256,  # [N, 512, 4, 4]
                      out_channels=512,
                      kernel_size=(3, 3),
                      stride=(2, 2),
                      padding=(1, 1),
                      bias=False),
            nn.BatchNorm2d(512),
            nn.LeakyReLU(0.2),
        )

卷积层之后的结果,输入分类器里面,Sigmoid 作为激活函数,得到一个概率。

self.linear = nn.Linear(512*4*4, 1)
self.out_ac = nn.Sigmoid()

开始写前向传播,输入图像的形状是 [N, 3, 64, 64],经过卷积层之后,输出形状是 [N, 512, 4, 4],接下来是进入分类器,我们需要对输出结果进行处理,将其拉平,拉成一个二维的。

    def forward(self, image):
        out_d = self.discriminator(image) # image [N, 3, 64, 64] -> [N, 256, 2, 2]
        out_d = out_d.view(-1, 256*2*2) # tensor flatten
        return self.out_ac(self.linear(out_d))

训练

image.png

整个实现代码过程,主要是实现论文中这个过程。

首先,我们需要保证代码的可复现性,需要对 random 设置种子。

torch.random.manual_seed(HP.seed)
torch.cuda.manual_seed(HP.seed)
random.seed(HP.seed)
np.random.seed(HP.seed)

准备工作,获取模型,设置模型初始化,设置 损失函数,设置优化器。

    # model init
	G = Generator() # new a generator model instance
    G.apply(G.weights_init) # apply weight init for G
    D = Discriminator()  # new a discriminator model instance
    D.apply(D.weights_init)  # apply weight init for G
    G.to(HP.device)
    D.to(HP.device)
    
 	# loss criterion
    criterion = nn.BCELoss() # binary classification loss
 
    # optimizer
    optimizer_g = optim.Adam(G.parameters(), lr=HP.init_lr, betas=(HP.beta, 0.999))
    optimizer_d = optim.Adam(D.parameters(), lr=HP.init_lr, betas=(HP.beta, 0.999))

第一步,完成以下公式的实现:

θd1mi=1m[logD(x(i))+log(1D(G(z(i))))]\nabla_{\theta_{d}} \frac{1}{m} \sum_{i=1}^{m}\left[\log D\left(\boldsymbol{x}^{(i)}\right)+\log \left(1-D\left(G\left(\boldsymbol{z}^{(i)}\right)\right)\right)\right]

公式前半部分:拿到输入 xx,送入模型 D 中,获取结果,设置真实照片(正样本)的 label 值,送入损失函数中。

labels_gt = torch.full(size=(b_size, ), fill_value=0.9, dtype=torch.float, device=HP.device) # batch 批次的设置 label
predict_labels_gt = D(batch.to(HP.device)).squeeze() # [64, 1] -> [64,]
loss_d_of_gt = criterion(predict_labels_gt, labels_gt)

公式后半部分:需要随机生成噪声 zz,送入 D(G(z)) 模型中,获取结果,接下来设置假照片(负样本)的 label 值,都送入损失函数中。

            labels_fake = torch.full(size=(b_size, ), fill_value=0.1, dtype=torch.float, device=HP.device)# 设置负样本label
            latent_z = torch.randn(size=(b_size, HP.z_dim), device=HP.device) # 噪声
            predict_labels_fake = D(G(latent_z)).squeeze() # [64, 1] - > [64,]
            loss_d_of_fake = criterion(predict_labels_fake, labels_fake)

两个损失值相加,开始梯度更新。

            loss_D = loss_d_of_gt + loss_d_of_fake  # add the two parts
            loss_D.backward()
            optimizer_d.step()

第二步:完成以下公式实现:

θg1mi=1mlog(1D(G(z(i))))\nabla_{\theta_{g}} \frac{1}{m} \sum_{i=1}^{m} \log \left(1-D\left(G\left(\boldsymbol{z}^{(i)}\right)\right)\right)

该公式的实现过程与上面是一致的,都是设置噪声,送入模型,设置 label 值,送入损失函数中,开始梯度更新。但是这里不同的是 label 值的设置,第二步是为了训练 G 模型,我们希望生成器生成的图片可以骗过判别器的,所以 D 模型输出的值越大越好,所以我们比较的 label 值应该设置成正样本。

            latent_z = torch.randn(size=(b_size, HP.z_dim), device=HP.device)
            labels_for_g = torch.full(size=(b_size, ), fill_value=0.9, dtype=torch.float, device=HP.device) # 正样本label值
            predict_labels_from_g = D(G(latent_z)).squeeze() # [N, ]
 
            loss_G = criterion(predict_labels_from_g, labels_for_g
            loss_G.backward()
            optimizer_g.step()

注:正样本 label 值设置为 0.9,负样本 label 值设置为 0.1。

目的:

  • 一定程度上,可以缓解模型过于武断的问题,也有一定的抗噪能力
  • 弥补了简单分类中监督信号不足(信息熵比较少)的问题,增加了信息量;
  • 提供了训练数据中类别之间的关系(数据增强);
  • 可能增强了模型泛化能力

参考文章:cloud.tencent.com/developer/a…

接下来,保存生成器和判别器模型数据和生成器生成的照片。

            if not step % HP.verbose_step:
                with torch.no_grad():
                    fake_image_dev = G(fixed_latent_z)
                    logger.add_image('Generator Faces', invTrans(vutils.make_grid(fake_image_dev.detach().cpu(), nrow=8)), step)
 
            if not step % HP.save_step: # save G and D
                model_path = 'model_g_%d_%d.pth' % (epoch, step)
                save_checkpoint(G, epoch,optimizer_g, os.path.join('model_save', model_path))
                model_path = 'model_d_%d_%d.pth' % (epoch, step)
                save_checkpoint(D, epoch, optimizer_d, os.path.join('model_save', model_path))

到这里训练代码基本结束了,这里没有给出完整的代码,主要是理解,可以自己尝试。